Model Context Protocol (MCP)
Full MCP support -- connect to external MCP servers to use their tools, or expose Cogitator tools as an MCP server.
Overview
The @cogitator-ai/mcp package provides bidirectional MCP integration:
- MCPClient -- connect to any MCP server (filesystem, database, GitHub, etc.) and use its tools inside Cogitator agents.
- MCPServer -- expose your Cogitator tools, resources, and prompts as an MCP server for Claude Desktop, other AI assistants, or any MCP client.
- Tool Adapter -- convert between Cogitator and MCP tool formats, including Zod-to-JSON Schema conversion.
pnpm add @cogitator-ai/mcp @cogitator-ai/coreMCPClient
Connecting to an MCP Server
Use MCPClient.connect() to establish a connection. The client discovers server capabilities automatically and provides access to tools, resources, and prompts.
import { MCPClient } from '@cogitator-ai/mcp';
const client = await MCPClient.connect({
transport: 'stdio',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/home/user/documents'],
timeout: 10000,
});
console.log(client.getCapabilities());
const tools = await client.getTools();
const resources = await client.listResources();
const prompts = await client.listPrompts();
await client.close();Using MCP Tools with Agents
The tools returned by getTools() are standard Cogitator Tool instances -- pass them directly to an agent:
import { Cogitator, Agent } from '@cogitator-ai/core';
import { MCPClient } from '@cogitator-ai/mcp';
const fsClient = await MCPClient.connect({
transport: 'stdio',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/workspace'],
});
const dbClient = await MCPClient.connect({
transport: 'http',
url: 'http://localhost:3001/mcp',
});
const fsTools = await fsClient.getTools();
const dbTools = await dbClient.getTools();
const agent = new Agent({
name: 'file-manager',
model: 'openai/gpt-6.1-sol',
instructions: 'Help users manage files and query databases.',
tools: [...fsTools, ...dbTools],
});
const cog = new Cogitator({/* ... */});
const result = await cog.run(agent, {
input: 'List all .ts files in /workspace/src and count them',
});
await fsClient.close();
await dbClient.close();connectMCPServer Helper
For the common pattern of connect-get-tools-cleanup, use the shorthand:
import { connectMCPServer } from '@cogitator-ai/mcp';
const { tools, cleanup } = await connectMCPServer({
transport: 'stdio',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-github'],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN! },
});
const agent = new Agent({
name: 'github-bot',
model: 'openai/gpt-6.1-sol',
instructions: 'Answer questions about GitHub repositories.',
tools,
});
await cleanup();It also returns the connected client. For stdio servers, env is merged over the current process environment.
MCPClientConfig
| Option | Type | Description |
|---|---|---|
transport | 'stdio' | 'http' | 'sse' | Transport protocol ('sse' is an alias of 'http') |
command | string | Command for stdio transport |
args | string[] | Command arguments for stdio |
env | Record<string, string> | Environment variables for stdio |
cwd | string | Working directory for the stdio process |
url | string | Server URL for HTTP/SSE transport |
headers | Record<string, string> | Extra HTTP headers (e.g. Authorization) |
timeout | number | Connection timeout in ms (default: none) |
clientName | string | Client identification name |
clientVersion | string | Client version string |
retry | MCPRetryConfig | Retry settings for failed operations |
autoReconnect | boolean | Auto-reconnect on connection loss (default: true) |
onReconnecting, onReconnected, onReconnectFailed | callbacks | Reconnection lifecycle hooks |
The http transport speaks MCP Streamable HTTP; servers that only offer the legacy HTTP+SSE transport are not supported. Besides getTools(), the client has listToolDefinitions() (raw MCP definitions), isConnected(), reconnect() and close().
Retry and Reconnection
The client has built-in retry with exponential backoff for all operations. On connection loss, it automatically reconnects:
const client = await MCPClient.connect({
transport: 'http',
url: 'http://mcp-server:3000/mcp',
retry: {
maxRetries: 5,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
retryOnConnectionLoss: true,
},
onReconnecting: (attempt) => console.log(`Reconnecting (attempt ${attempt})...`),
onReconnected: () => console.log('Reconnected'),
onReconnectFailed: (err) => console.error('Reconnection failed:', err),
});Only transient failures are retried (connection loss, request timeouts, network errors). Protocol errors returned by the server, tool failures and aborted calls fail immediately, and concurrent calls that lose the connection share a single reconnect.
Tool Results and Errors
callTool returns structuredContent when the server provides it, otherwise the unwrapped content (text blocks are JSON-parsed when possible, a single block is returned on its own, an empty result is null). A tool result with isError: true throws MCPToolError, so MCP failures show up as ordinary tool errors inside agent runs. Pass an AbortSignal to cancel a call; tools created by getTools() forward the run's signal automatically.
import { MCPToolError } from '@cogitator-ai/mcp';
try {
await client.callTool('write_file', { path: '/etc/passwd', content: '' }, { timeout: 5_000 });
} catch (error) {
if (error instanceof MCPToolError) {
console.error(`${error.toolName} failed: ${error.message}`);
}
}Resources and Prompts
Beyond tools, MCP servers can expose resources (data sources) and prompts (reusable prompt templates):
const resources = await client.listResources();
const content = await client.readResource('file:///workspace/config.json');
console.log(content.text);
const prompts = await client.listPrompts();
const messages = await client.getPrompt('code-review', {
language: 'typescript',
style: 'thorough',
});readResource returns the first content block; readResourceContents returns all of them.
MCPServer
Exposing Cogitator Tools
Create an MCP server to make your Cogitator tools available to any MCP client:
import { MCPServer } from '@cogitator-ai/mcp';
import { tool } from '@cogitator-ai/core';
import { z } from 'zod';
const calculator = tool({
name: 'add',
description: 'Add two numbers',
parameters: z.object({ a: z.number(), b: z.number() }),
execute: async ({ a, b }) => ({ sum: a + b }),
});
const server = new MCPServer({
name: 'my-tools',
version: '1.0.0',
transport: 'stdio',
});
server.registerTools([calculator]);
await server.start();serveMCPTools(tools, config) does the same in one call and returns the started server. Tools, resources and prompts can be added one by one (registerTool, registerResource, registerPrompt) or in batches (registerTools, registerResources, registerPrompts), removed with the matching unregister* methods and listed with getRegisteredTools() / getRegisteredResources() / getRegisteredPrompts().
HTTP Transport
For remote access, use the HTTP transport:
const server = new MCPServer({
name: 'remote-tools',
version: '1.0.0',
transport: 'http',
port: 3000,
host: 'localhost',
logging: true,
});
server.registerTools([calculator, webSearch, fileReader]);
await server.start();Clients connect to http://localhost:3000/mcp (port defaults to 3000, host to localhost; other paths answer 404). Use port: 0 to bind a random free port and read it back with server.getPort(). maxBodySize (default 10 MB), corsOrigin (default '*') and auth (see below) tune the HTTP endpoint; start() rejects if the port cannot be bound and stop() closes open connections.
Tool arguments are validated against the tool's full Zod schema before execute runs; invalid arguments and thrown errors come back to the client as isError tool results, while errors thrown by resource read and prompt get handlers are returned as JSON-RPC errors. With logging: true, diagnostics go to stderr so the stdio transport's stdout stays reserved for JSON-RPC.
Authentication and Per-User Tools
Over HTTP the server is open to anyone who can reach it. Pass auth to establish the caller of every request, usually from its bearer token; it receives the raw http.IncomingMessage and returns an MCPCaller ({ userId?, scopes?, metadata? }). Requests it returns undefined for (or throws on) are answered 401 with WWW-Authenticate: Bearer. The caller's userId reaches tools as context.userId, and resource read and prompt get handlers receive the caller as their second argument:
import { MCPServer, type MCPAuthFunction } from '@cogitator-ai/mcp';
const auth: MCPAuthFunction = async (request) => {
const token = request.headers.authorization?.replace(/^Bearer /, '');
const session = token ? await sessions.find(token) : undefined;
return session ? { userId: session.userId, scopes: session.scopes } : undefined;
};
const listMyOrders = tool({
name: 'list_my_orders',
description: "List the calling customer's orders",
parameters: z.object({}),
execute: async (_args, context) => orders.forUser(context.userId!),
});
const server = new MCPServer({ name: 'shop', version: '1.0.0', transport: 'http', auth });
server.registerTool(listMyOrders);An agent then acts for one user by connecting with that user's token:
const client = await MCPClient.connect({
transport: 'http',
url: 'https://shop.example.com/mcp',
headers: { Authorization: `Bearer ${userToken}` },
});
const agentForUser = support.clone({ id: support.id, tools: await client.getTools() });
await cog.run(agentForUser, { input, userId, threadId });The full flow, including thread isolation between users, is in examples/mcp/03-per-user-mcp.ts.
Registering Resources
Expose data sources as MCP resources:
server.registerResource({
uri: 'config://app/settings',
name: 'App Settings',
description: 'Current application configuration',
mimeType: 'application/json',
read: async () => ({
uri: 'config://app/settings',
text: JSON.stringify(await loadAppSettings()),
}),
});A URI with {placeholders} is registered as a resource template; the matched values arrive in params. read may return one content or an array, and uri defaults to the URI that was read:
server.registerResource({
uri: 'db://users/{userId}',
name: 'User Profile',
description: 'Fetch a user profile by ID',
read: async (params) => ({
uri: `db://users/${params.userId}`,
text: JSON.stringify(await db.getUser(params.userId)),
}),
});Registering Prompts
Expose reusable prompt templates:
server.registerPrompt({
name: 'code-review',
title: 'Code Review',
description: 'Generate a code review for the given language',
arguments: [
{ name: 'language', description: 'Programming language', required: true },
{ name: 'style', description: 'Review depth: quick or thorough' },
],
get: async (args) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Review this ${args.language} code. Style: ${args.style ?? 'thorough'}.`,
},
},
],
}),
});Serving Agents
serveAgents turns Cogitator agents into an MCP server in one call. Each agent becomes a tool that takes a task (and optionally the threadId of an earlier answer, to continue that conversation — needs memory on the Cogitator) and answers with the agent's reply:
import { serveAgents } from '@cogitator-ai/mcp';
await serveAgents(cog, [researcher, writer]); // stdio — for Claude Desktop, Cursor, Claude Code
await serveAgents(cog, support, { transport: 'http', port: 3333, auth }); // remote, per-userIn the client's MCP config:
{ "mcpServers": { "cogitator": { "command": "npx", "args": ["tsx", "my-agents.ts"] } } }The tool answers { status: 'completed', output, threadId }. Each tool is named after its agent, with characters other than letters, digits, _ and - replaced (toolNames: { [agent.name]: 'tool_name' } renames them), and described by the agent's description — or by the first sentence of its instructions when it has none. serveAgents takes every MCPServer option (defaults: name cogitator-agents, transport: 'stdio', sessions: true) and returns the started server. With auth, runs carry the caller's userId, so threads and memory stay per user (Multiple Users), and the client's cancellation aborts the run.
Approvals. When an agent's tool needs approval, the server asks the person at the client through MCP elicitation and the run goes on with their answer. Clients without elicitation get a paused answer ({ status: 'paused', threadId, pendingApprovals, next }) and continue with the <agent>_resume tool (the agent's tool name plus _resume) — registered only for agents with a tool whose requiresApproval is true or a check function — passing { threadId, approved, reason? } once the user decided. Elicitation needs a connection the client can reply on: stdio, or HTTP with sessions: true — which serveAgents turns on.
agentTools(cog, agents, toolNames?) returns the same tools to register on an MCPServer you configure yourself. Both accept anything with Cogitator's run and resume (AgentHost).
Sessions
MCPServer over HTTP serves each request on its own by default. sessions: true keeps a session per client (mcp-session-id), which requests from the server to the client — such as elicitation — need. A session belongs to the caller that started it: a request with another caller's credentials gets 403. Elicitation works over stdio or HTTP with sessions: true; elsewhere elicit is not available.
Your own tools reach the elicitation channel through their context. elicit resolves to { action: 'accept', content }, { action: 'decline' | 'cancel' }, or undefined when the client cannot answer:
import type { MCPToolContext } from '@cogitator-ai/mcp';
const deleteOrder = tool({
name: 'delete_order',
description: 'Delete an order after the user confirms',
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }, context) => {
const reply = await (context as MCPToolContext).elicit?.({
message: `Delete order ${orderId}?`,
schema: {
type: 'object',
properties: { confirm: { type: 'boolean', title: 'Confirm' } },
required: ['confirm'],
},
});
if (reply?.action !== 'accept' || reply.content.confirm !== true) return { deleted: false };
await orders.delete(orderId);
return { deleted: true };
},
});Tool Adapter
Convert tools between Cogitator and MCP formats manually when you need fine-grained control.
Cogitator to MCP
import { cogitatorToMCP } from '@cogitator-ai/mcp';
const mcpDef = cogitatorToMCP(calculator);MCP to Cogitator
import { mcpToCogitator, wrapMCPTools } from '@cogitator-ai/mcp';
const cogTool = mcpToCogitator(mcpToolDefinition, client, {
namePrefix: 'github_',
descriptionTransform: (desc) => `[GitHub] ${desc}`,
});
const allTools = await wrapMCPTools(client, { namePrefix: 'fs_' });Schema Conversion
The adapter includes utilities for converting between Zod schemas and JSON Schema, which is useful when working with MCP's JSON Schema-based tool definitions:
import { zodToJsonSchema, jsonSchemaToZod } from '@cogitator-ai/mcp';
import { z } from 'zod';
const schema = z.object({
query: z.string().describe('Search query'),
limit: z.number().int().min(1).max(100).optional(),
});
const jsonSchema = zodToJsonSchema(schema);
const zodSchema = jsonSchemaToZod({
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'integer', minimum: 1, maximum: 100 },
},
required: ['query'],
});Claude Desktop Integration
To use your Cogitator tools in Claude Desktop, run an MCP server with stdio transport and add it to your Claude config:
{
"mcpServers": {
"my-cogitator-tools": {
"command": "node",
"args": ["./dist/mcp-server.js"]
}
}
}import { MCPServer } from '@cogitator-ai/mcp';
import { calculator, webSearch } from './tools';
const server = new MCPServer({
name: 'my-cogitator-tools',
version: '1.0.0',
transport: 'stdio',
});
server.registerTools([calculator, webSearch]);
await server.start();