Types
Core TypeScript types used in the SDK.
Most only need ToolContext. The other types are for advanced use cases
like custom transports or protocol-level work.
Types marked with 📘 are not yet documented in the Python SDK reference (though most exist in the Python implementation). This helps track documentation parity.
Common Types
ToolContext<TInput, TSecrets, TAuth> 📘
passed to your handlers. Destructure what you need:
interface AuthInfo {
token: string;
/** Provider-specific user data (e.g., sub, email, name). Shape depends on provider and scopes. */
userInfo: Record<string, unknown>;
}
interface ToolContext<
TInput,
TSecrets extends string = string,
TAuth extends boolean = false
> {
/** Validated input matching your Zod schema */
input: TInput;
/** OAuth token and user info — non-optional when requiresAuth is set */
authorization: TAuth extends true ? AuthInfo : AuthInfo | undefined;
/**
* Get a secret value. Only keys declared in requiresSecrets are allowed.
* Validated at startup — guaranteed to exist at runtime.
*/
getSecret(key: TSecrets): string;
/** Metadata from Arcade auth (e.g., client_id, coordinator_url) */
metadata: Record<string, unknown>;
/** User identifier for this request (from auth, config, or session) */
userId: string | null;
/** Unique identifier for this MCP session (null for stdio without session management) */
sessionId: string | null;
/** Unique identifier for this specific request */
requestId: string | null;
// ─── Runtime Capabilities ─────────────────────────────────────────
/** MCP protocol logging — sent to the AI client, not console */
log: {
debug(message: string): Promise<void>;
info(message: string): Promise<void>;
warning(message: string): Promise<void>;
error(message: string): Promise<void>;
};
/** Progress reporting for long-running operations */
progress: {
report(progress: number, total?: number, message?: string): Promise<void>;
};
/** Read resources exposed by the server */
resources: {
/** Read a resource, returns array (may have multiple parts) */
read(uri: string): Promise<ResourceContents[]>;
/** Convenience: read and return first content item, throws if not found */
get(uri: string): Promise<ResourceContents>;
/** List available resources */
list(): Promise<Resource[]>;
/** List URI templates for dynamic resources */
listTemplates(): Promise<ResourceTemplate[]>;
/** List client roots (directories the client has shared) */
listRoots(): Promise<Root[]>;
};
/** Call other tools from within a tool handler */
tools: {
/** Execute a tool and get raw result */
call(name: string, args?: Record<string, unknown>): Promise<CallToolResult>;
/** List available tools */
list(): Promise<Tool[]>;
};
/** Access prompts exposed by the server */
prompts: {
get(name: string, args?: Record<string, string>): Promise<GetPromptResult>;
list(): Promise<Prompt[]>;
};
/** Request LLM completions from the client (if supported) */
sampling: {
createMessage(request: CreateMessageRequest): Promise<CreateMessageResult>;
};
/** Create rich UI elements (elicitation, forms) */
ui: {
/**
* Request structured input from the user.
* Schema must be an object with primitive properties only
* (string, number, integer, boolean). String formats: email, uri, date, date-time.
*/
elicit<T extends z.ZodObject<any>>(
message: string,
schema: T,
options?: { timeout?: number }
): Promise<ElicitResult<z.infer<T>>>;
};
/** Notify client when server capabilities change */
notifications: {
tools: { listChanged(): Promise<void> };
resources: { listChanged(): Promise<void> };
prompts: { listChanged(): Promise<void> };
};
}
// ─── Supporting Types ───────────────────────────────────────────────
/** Content returned when reading a resource */
interface ResourceContents {
uri: string;
mimeType?: string;
text?: string;
blob?: string; // Base64-encoded binary
}
/** Resource metadata */
interface Resource {
uri: string;
name: string;
description?: string;
mimeType?: string;
}
/** URI template for dynamic resources */
interface ResourceTemplate {
uriTemplate: string;
name: string;
description?: string;
mimeType?: string;
}
/** Client root directory */
interface Root {
uri: string;
name?: string;
}
/** Tool metadata */
interface Tool {
name: string;
description?: string;
inputSchema: JsonSchema;
}
/** Prompt metadata */
interface Prompt {
name: string;
description?: string;
arguments?: PromptArgument[];
}
/** Result from getting a prompt */
interface GetPromptResult {
description?: string;
messages: PromptMessage[];
}
/** Request for LLM sampling */
interface CreateMessageRequest {
messages: SamplingMessage[];
systemPrompt?: string;
maxTokens: number;
temperature?: number;
stopSequences?: string[];
}
/** Message for/from sampling */
interface SamplingMessage {
role: 'user' | 'assistant';
content: TextContent | ImageContent | AudioContent;
}
/** Result from sampling.createMessage */
interface CreateMessageResult {
role: 'assistant';
content: TextContent | ImageContent | AudioContent;
model: string;
stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens';
}
/** Result from tools.call */
interface CallToolResult {
content: ContentItem[];
structuredContent?: Record<string, unknown>;
isError?: boolean;
}
/** Result from elicitation UI */
interface ElicitResult<T> {
action: 'accept' | 'decline' | 'cancel';
content?: T;
}
/** JSON Schema (subset used by MCP) */
type JsonSchema = Record<string, unknown>;
/** Prompt argument definition */
interface PromptArgument {
name: string;
description?: string;
required?: boolean;
}
/** Message in a prompt */
interface PromptMessage {
role: 'user' | 'assistant';
content: TextContent | ImageContent | AudioContent | EmbeddedResource | ResourceLink;
}
/** Text content block */
interface TextContent {
type: 'text';
text: string;
}
/** Image content block */
interface ImageContent {
type: 'image';
data: string; // Base64 encoded
mimeType: string;
}
/** Audio content block */
interface AudioContent {
type: 'audio';
data: string; // Base64 encoded
mimeType: string;
}
/** Embedded resource content */
interface EmbeddedResource {
type: 'resource';
resource: ResourceContents;
}
/** Link to a resource by URI */
interface ResourceLink {
type: 'resource_link';
uri: string;
name: string;
description?: string;
mimeType?: string;
}Example — Type-safe authorization:
import { z } from 'zod';
import { Google } from 'arcade-mcp-server/auth';
// Without requiresAuth: authorization is optional
app.tool('publicTool', {
input: z.object({ query: z.string() }),
handler: ({ authorization }) => {
authorization?.token; // Must use optional chaining
},
});
// With requiresAuth: authorization is guaranteed
app.tool('privateTool', {
input: z.object({ query: z.string() }),
requiresAuth: Google({ scopes: ['profile'] }),
handler: ({ authorization }) => {
authorization.token; // ✅ No optional chaining needed
},
});Example — Type-safe secrets:
app.tool('search', {
input: z.object({ query: z.string() }),
requiresSecrets: ['API_KEY'] as const, // as const is required!
handler: ({ getSecret }) => {
getSecret('API_KEY'); // ✅ Autocomplete works
// getSecret('OTHER'); // ❌ TypeScript error
},
});Why as const? Without it, TypeScript infers string[] instead of the literal tuple ['API_KEY']. The as const preserves the exact strings so getSecret() can type-check against them.
Secrets are validated at startup. Missing secrets fail fast with a clear error.
Runtime Capabilities 📘
Tools have access to 8 runtime capabilities via the object. Destructure what you need:
app.tool('processData', {
input: z.object({ uri: z.string() }),
handler: async ({
input,
log, // MCP protocol logging
progress, // Progress reporting
resources, // Read server resources
tools, // Call other tools
prompts, // Access prompts
sampling, // Request LLM completions
ui, // Elicitation / forms
notifications // Notify list changes
}) => {
// Log progress to the client
await log.info('Starting...');
await progress.report(0, 3);
// Read a resource (get() returns single item, read() returns array)
const content = await resources.get(input.uri);
await progress.report(1, 3);
// Call another tool (returns CallToolResult)
const result = await tools.call('analyze', { data: content.text });
if (result.isError) throw new Error('Analysis failed');
await progress.report(2, 3);
// Ask user for confirmation via elicitation
const confirm = await ui.elicit('Proceed?', z.object({
confirmed: z.boolean(),
}));
if (confirm.action !== 'accept' || !confirm.content?.confirmed) {
return 'Cancelled by user';
}
await progress.report(3, 3, 'Done');
return result.structuredContent ?? result.content;
},
});| Capability | Description |
|---|---|
log | Send debug/info/warning/error messages to the client |
progress | Report progress for long-running operations |
resources | Read resources exposed by the server |
tools | Call other tools from within a tool handler |
prompts | Access prompt templates defined on the server |
sampling | Request LLM completions from the client |
ui | Create elicitation forms for user input |
notifications | Notify client when tools/resources/prompts change |
Not all clients support all capabilities. sampling and ui.elicit depend on client support.
Elicitation Schema Restrictions: limits elicitation schemas to object types with primitive
properties only (string, number, integer, boolean). String properties support formats:
email, uri, date, date-time. Nested objects and arrays are not allowed.
Type Inference with Zod 📘
The SDK fully leverages Zod’s type inference. Your handler receives typed input automatically:
import { z } from 'zod';
const searchInput = z.object({
query: z.string(),
limit: z.number().int().min(1).max(100).default(10),
filters: z.object({
category: z.enum(['all', 'docs', 'code']).optional(),
after: z.coerce.date().optional(), // Coerces ISO strings to Date
}).optional(),
});
app.tool('search', {
input: searchInput,
handler: ({ input }) => {
// TypeScript knows:
// - input.query is string
// - input.limit is number (default applied)
// - input.filters?.category is 'all' | 'docs' | 'code' | undefined
// - input.filters?.after is Date | undefined
return search(input);
},
});Tool Response Types 📘
Handlers can return any value. The SDK auto-wraps:
| Return type | Becomes |
|---|---|
string | { content: [{ type: 'text', text }] } |
object | { content: [{ type: 'text', text: JSON.stringify(obj) }] } |
{ content: [...] } | Passed through unchanged |
ContentItem 📘
For full control over responses, return content items directly:
type ContentItem =
| TextContent
| ImageContent
| AudioContent
| EmbeddedResource
| ResourceLink;Individual content types (TextContent, ImageContent, etc.) are defined in the ToolContext section above.
Example:
import { z } from 'zod';
app.tool('screenshot', {
input: z.object({}),
handler: async () => {
const screenshot = await captureScreen();
return {
content: [{
type: 'image',
data: screenshot.toBase64(),
mimeType: 'image/png',
}],
};
},
});CallToolResult
The complete result structure:
interface CallToolResult {
/** Content items to return to the client */
content: ContentItem[];
/** Optional structured data (for programmatic access) */
structuredContent?: Record<string, unknown>;
/** Whether this result represents an error */
isError?: boolean;
}Important: When isError: true, the AI client knows the failed and can decide whether to retry. Always set this flag when returning error information — don’t just return an error message as regular content.
Returning errors correctly:
app.tool('fetchUser', {
input: z.object({ id: z.string() }),
handler: async ({ input }) => {
const user = await db.findUser(input.id);
if (!user) {
// ✅ Correct: Set isError so AI knows this failed
return {
content: [{ type: 'text', text: `User ${input.id} not found` }],
isError: true,
};
}
return user;
},
});For most error cases, throw specific error types instead — see Errors.
Schema Types 📘
The SDK uses Zod 4 for input validation.
Converting Schemas to JSON Schema 📘
Use z.toJSONSchema() to convert Zod schemas for AI clients:
import { z } from 'zod';
const schema = z.object({
firstName: z.string().describe('Your first name'),
lastName: z.string().meta({ title: 'last_name' }),
age: z.number().meta({ examples: [12, 99] }),
});
z.toJSONSchema(schema);
// => {
// type: 'object',
// properties: {
// firstName: { type: 'string', description: 'Your first name' },
// lastName: { type: 'string', title: 'last_name' },
// age: { type: 'number', examples: [12, 99] }
// },
// required: ['firstName', 'lastName', 'age']
// }Adding Metadata 📘
Use .describe() for simple descriptions or .meta() for richer metadata:
// Simple description (Zod 3 compatible)
z.string().describe('User email address');
// Rich metadata (Zod 4)
z.string().meta({
id: 'email_address',
title: 'Email',
description: 'User email address',
examples: ['user@example.com'],
});
// .describe() is shorthand for .meta({ description: ... })
z.string().describe('An email');
// equivalent to:
z.string().meta({ description: 'An email' });Both are preserved in JSON Schema output.
Extracting TypeScript Types 📘
Use z.infer to extract TypeScript types from schemas:
import { z } from 'zod';
const userSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
});
// Extract the type
type User = z.infer<typeof userSchema>;
// Use in your code
function processUser(user: User) {
// user.id is string
// user.role is 'admin' | 'user' | 'guest'
}Protocol Types 📘
These are low-level types for custom transports or debugging.
MCPMessage 📘
Base type for all protocol messages:
type MCPMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;
interface JSONRPCRequest {
jsonrpc: '2.0';
id: string | number;
method: string;
params?: Record<string, unknown>;
}
interface JSONRPCResponse {
jsonrpc: '2.0';
id: string | number;
result?: unknown;
error?: {
code: number;
message: string;
data?: unknown;
};
}
interface JSONRPCNotification {
jsonrpc: '2.0';
method: string;
params?: Record<string, unknown>;
}ServerSession 📘
interface ServerSession {
/** Unique session identifier */
id: string;
/** Client information (if provided during initialization) */
clientInfo?: {
name: string;
version: string;
};
/** Client capabilities (tools, resources, prompts support) */
capabilities?: Record<string, unknown>;
}SessionMessage
Also documented in Python SDK
interface SessionMessage {
message: MCPMessage;
session: ServerSession;
}Auth Types 📘
AuthProvider 📘
interface AuthProvider {
/** Provider identifier (e.g., 'google', 'github') */
provider: string;
/** OAuth scopes required */
scopes: readonly string[];
}Creating Auth Providers 📘
import { Google, GitHub, Slack } from 'arcade-mcp-server/auth';
// Google with specific scopes
Google({ scopes: ['profile', 'email'] })
// GitHub with repo access
GitHub({ scopes: ['repo', 'user'] })
// Slack
Slack({ scopes: ['chat:write', 'users:read'] })Error Adapter Types 📘
ErrorAdapter 📘
Translates vendor-specific exceptions into Arcade errors:
interface ErrorAdapter {
/** Identifier for logging/metrics */
slug: string;
/** Translate an exception into an Arcade tool error, or null if not handled */
fromException(error: unknown): ToolError | null;
}
/** Base class for tool execution errors */
class ToolError extends Error {
canRetry: boolean;
statusCode?: number;
developerMessage?: string;
}
/** Error from an upstream service (API, database, etc.) */
class UpstreamError extends ToolError {}
/** Error that should not be retried */
class FatalToolError extends ToolError {}
/** Error that can be retried */
class RetryableToolError extends ToolError {
retryAfterMs?: number;
}Built-in Adapters 📘
import {
SlackErrorAdapter,
GoogleErrorAdapter,
MicrosoftGraphErrorAdapter,
HTTPErrorAdapter,
GraphQLErrorAdapter,
} from 'arcade-mcp-server/adapters';
app.tool('sendMessage', {
input: z.object({ channel: z.string(), text: z.string() }),
adapters: [new SlackErrorAdapter()],
handler: async ({ input }) => {
// SlackApiError → UpstreamError automatically
await slack.chat.postMessage(input);
return 'Sent!';
},
});| Adapter | Handles |
|---|---|
SlackErrorAdapter | Slack SDK errors |
GoogleErrorAdapter | Google API Client errors |
MicrosoftGraphErrorAdapter | Microsoft Graph SDK errors |
HTTPErrorAdapter | fetch/HTTP library errors |
GraphQLErrorAdapter | GraphQL client errors |
Adapters are tried in order. First match wins. HTTPErrorAdapter is always added as fallback.
Utility Types 📘
MaterializedTool 📘
A object with type inference. Created via tool():
import { tool } from 'arcade-mcp-server';
import { z } from 'zod';
const myTool = tool({
description: 'Does something',
input: z.object({ value: z.string() }),
handler: ({ input }) => `Got: ${input.value}`, // input is typed
});
// Register with app.tool() or runtime APIs — name is always first arg
app.tool('my-tool', myTool);
await server.tools.add('my-tool', myTool);ToolOptions 📘
Complete configuration type:
interface ToolOptions<
TInput,
TSecrets extends string = string,
TAuth extends AuthProvider | undefined = undefined
> {
description?: string;
input: z.ZodType<TInput>;
handler: (
context: ToolContext<TInput, TSecrets, TAuth extends AuthProvider ? true : false>
) => unknown | Promise<unknown>;
requiresAuth?: TAuth;
requiresSecrets?: readonly TSecrets[];
requiresMetadata?: string[];
adapters?: ErrorAdapter[];
}The TAuth type parameter enables conditional typing: when requiresAuth is set, authorization becomes non-optional in the handler .
ArcadeMCPOptions 📘
interface ArcadeMCPOptions {
name?: string;
version?: string;
title?: string;
instructions?: string;
logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
transport?: 'stdio' | 'http';
host?: string;
port?: number;
reload?: boolean;
adapter?: object; // For Node.js: node() from @elysiajs/node
}