diff --git a/docs/README.md b/docs/README.md index 317b6d7..c5ae30a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,6 +32,8 @@ You can use it to: [Achieving Agent Interoperability](./learn-more/achieving-agent-interoperability.md) +[Speed Up MCPC with In-Memory Transport](./learn-more/speed-up-with-in-memory-transport.md) + [Plugin System](./plugins.md) [Logging and Tracing](./logging-and-tracing.md) diff --git a/docs/faq.md b/docs/faq.md index cdf5a6a..e8c43c8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -51,6 +51,14 @@ That's it. The agent can now call these tools. All MCP transports work: -- **stdio** -- **streamable-http** -- **sse** +- **stdio**: Spawns external processes +- **streamable-http**: HTTP-based communication +- **sse**: Server-Sent Events +- **memory**: In-memory transport for same-process communication + +> **Performance Tip**: If MCPC feels slow, try using `memory` transport. +> Connecting multiple MCP servers has overhead - memory transport eliminates it. + +> **Learn More**: See +> [Speed Up MCPC with In-Memory Transport](./learn-more/speed-up-with-in-memory-transport.md) +> for detailed examples and use cases. diff --git a/docs/learn-more/speed-up-with-in-memory-transport.md b/docs/learn-more/speed-up-with-in-memory-transport.md new file mode 100644 index 0000000..b5ec58b --- /dev/null +++ b/docs/learn-more/speed-up-with-in-memory-transport.md @@ -0,0 +1,89 @@ +# Speed Up MCPC with In-Memory Transport + +Connect MCP servers in the same process. No external processes, no network +overhead. + +## Why Use It + +**Zero overhead**: Same-process communication is instant\ +**Simple testing**: No external dependencies to mock\ +**Easy embedding**: Integrate MCP directly into your app + +## Usage + +**Step 1**: Create an MCP server + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +const myServer = new McpServer({ name: "my-server", version: "1.0.0" }); + +myServer.tool( + "greet", + "Greet a user", + { name: "string" }, + ({ name }) => ({ + content: [{ type: "text", text: `Hello, ${name}!` }], + }), +); +``` + +**Step 2**: Pass server instance to MCPC + +```typescript +import { mcpc } from "@mcpc/core"; + +const server = await mcpc( + [{ name: "my-agent", version: "1.0.0" }, { capabilities: { tools: {} } }], + [{ + name: "greeter", + description: 'Available tools:\n', + deps: { + mcpServers: { + "my-server": { + transportType: "memory", + server: myServer, // Pass the server instance + }, + }, + }, + }], +); +``` + +## The Advantage + +**Other transports** spawn processes or make network calls: + +```typescript +deps: { + mcpServers: { + "desktop-commander": { + command: "npx", // Spawns external process + args: ["-y", "@wonderwhy-er/desktop-commander"], + transportType: "stdio" + } + } +} +``` + +**Memory transport** runs in the same process: + +```typescript +deps: { + mcpServers: { + "my-server": { + transportType: "memory", + server: myServerInstance, // Instant, no overhead + }, + }, +} +``` + +## Try the Example + +```bash +deno run --allow-all packages/core/examples/13-in-memory-transport.ts +``` + +See the [example code](../../packages/core/examples/13-in-memory-transport.ts) +for a complete working implementation. diff --git a/docs/quickstart/create-your-first-agentic-mcp.md b/docs/quickstart/create-your-first-agentic-mcp.md index 04f96b6..fdd9090 100644 --- a/docs/quickstart/create-your-first-agentic-mcp.md +++ b/docs/quickstart/create-your-first-agentic-mcp.md @@ -43,7 +43,7 @@ See the magic in action 👇 The MCPC framework becomes truly powerful when you reuse and compose existing MCP Servers, much like your favorite AI-integrated clients (e.g., Cursor or VSCode). We offer full support for the MCP transport protocol, including -`stdio`, `sse`, and `streamable-http`. +`stdio`, `sse`, `streamable-http`, and `memory` (in-memory). ```typescript import { type ComposeDefinition, mcpc } from "@mcpc/core"; @@ -71,6 +71,11 @@ const deps: ComposeDefinition["deps"] = { }; ``` +> **💡 Tip**: For testing or embedding MCP servers in the same process, you can +> use `memory` transport. See +> [FAQ Q5](../faq.md#q5-what-transport-types-does-mcpc-support) for in-memory +> transport examples. + # Then write the documentation for your agent A documentation for your agent helps LLM understand the purpose of the agent, diff --git a/packages/cli/deno.json b/packages/cli/deno.json index 7b716c7..eb676b8 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/cli", - "version": "0.1.9", + "version": "0.1.10", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" diff --git a/packages/core/deno.json b/packages/core/deno.json index 5fa52f2..9fb44ab 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/core", - "version": "0.2.8", + "version": "0.2.9", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" diff --git a/packages/core/examples/13-in-memory-transport.ts b/packages/core/examples/13-in-memory-transport.ts new file mode 100644 index 0000000..c8f20bc --- /dev/null +++ b/packages/core/examples/13-in-memory-transport.ts @@ -0,0 +1,95 @@ +/** + * MCPC Example 13: In-Memory Transport + * + * Demonstrates the in-memory transport feature: + * - Using InMemoryTransport for in-process communication + * - Useful for testing and embedding MCP servers + * - No external process spawning required + * + * This creates a file manager that communicates with an embedded MCP server + * via in-memory transport, perfect for testing and integration scenarios. + */ + +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { type ComposeDefinition, mcpc } from "../mod.ts"; + +// Create a simple in-memory MCP server for demonstration +function createTestMcpServer() { + const mcpServer = new McpServer({ + name: "test-memory-server", + version: "1.0.0", + }); + + // Register a simple tool + mcpServer.tool( + "greet", + "Greet a user by name", + { name: "string" }, + ({ name }) => ({ + content: [{ + type: "text" as const, + text: + `Hello, ${name}! This message comes from an in-memory MCP server.`, + }], + }), + ); + + return mcpServer; +} + +// Initialize the in-memory server +const testServer = createTestMcpServer(); + +export const toolDefinitions: ComposeDefinition[] = [ + { + name: "memory-agent", + description: + `I am an agent that uses in-memory transport to communicate with MCP servers. + +Available tools: + + +I can greet users using the in-memory MCP server. This demonstrates how MCPC can work with +in-memory transports for testing and embedded scenarios where you don't want to spawn +external processes.`, + + deps: { + mcpServers: { + "test-memory-server": { + transportType: "memory" as const, + server: testServer, + }, + }, + }, + }, +]; + +export const server = await mcpc( + [ + { + name: "in-memory-example", + version: "1.0.0", + }, + { + capabilities: { + tools: { + listChanged: true, + }, + }, + }, + ], + toolDefinitions, +); + +// Only run if executed directly +if (import.meta.main) { + console.log("Starting In-Memory Transport Example Server..."); + console.log("\nThis example demonstrates in-memory transport:"); + console.log("- No external processes spawned"); + console.log("- Useful for testing and embedding"); + console.log("- Fast and efficient for in-process communication\n"); + + await server.connect(new StdioServerTransport()); + console.log("Server running with in-memory transport support!"); +} diff --git a/packages/core/src/service/tools.ts b/packages/core/src/service/tools.ts index 87048ed..bc95ea4 100644 --- a/packages/core/src/service/tools.ts +++ b/packages/core/src/service/tools.ts @@ -32,10 +32,17 @@ export const StdioConfigSchema: z.ZodObject> = transportType: z.literal("stdio").optional(), }); +export const InMemoryConfigSchema: z.ZodObject> = + BaseConfigSchema.extend({ + transportType: z.literal("memory"), + server: z.any(), // Server instance from @modelcontextprotocol/sdk + }); + export const ServerConfigSchema: z.ZodTypeAny = z.union([ StdioConfigSchema, SseConfigSchema, StreamableHTTPSchema, + InMemoryConfigSchema, ]); export const McpSettingsSchema: z.ZodObject> = z diff --git a/packages/core/src/utils/common/mcp.ts b/packages/core/src/utils/common/mcp.ts index 673e5a0..ea7f01d 100644 --- a/packages/core/src/utils/common/mcp.ts +++ b/packages/core/src/utils/common/mcp.ts @@ -2,6 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import type { McpSettingsSchema, ServerConfigSchema, @@ -28,7 +29,95 @@ function defSignature( def: z.input | z.infer, ) { // KISS: stringify full definition for a stable signature - return JSON.stringify(def); + // Handle circular references from InMemoryTransport or other objects + const defCopy = { ...def }; + + // For in-memory transport, create a unique signature without circular refs + if ( + (defCopy as any).transportType === "memory" || (defCopy as any).transport + ) { + return `memory:${Date.now()}:${Math.random()}`; + } + + return JSON.stringify(defCopy); +} + +/** + * Creates appropriate transport based on server config definition. + * Supports: stdio, sse, streamable-http, and in-memory transports. + * + * Compatible with multiple IDE/client config formats: + * - MCPC: explicit "transportType" field + * - VSCode/Cursor: explicit "type" field + * - Cline/Claude Desktop: implicit detection (command → stdio, url → http/sse) + */ +function createTransport( + def: z.input | z.infer, +): + | StdioClientTransport + | StreamableHTTPClientTransport + | SSEClientTransport + | InMemoryTransport { + const defAny = def as any; + + // Normalize transport type from different IDE formats + // Priority: transportType (MCPC) → type (VSCode/Cursor) → implicit detection (Cline) + const explicitType = defAny.transportType || defAny.type; + + // Check for in-memory transport - user provides a Server instance + if (explicitType === "memory") { + if (!defAny.server) { + throw new Error( + "In-memory transport requires a 'server' field with a Server instance", + ); + } + + const [clientTransport, serverTransport] = InMemoryTransport + .createLinkedPair(); + // Connect the server to serverTransport asynchronously + defAny.server.connect(serverTransport).catch((err: Error) => { + console.error("Error connecting in-memory server:", err); + }); + return clientTransport; + } + + // Check for SSE transport (explicit or has url with sse type) + if (explicitType === "sse") { + const options: any = {}; + if (defAny.headers) { + options.requestInit = { headers: defAny.headers }; + options.eventSourceInit = { headers: defAny.headers }; + } + return new SSEClientTransport(new URL(defAny.url), options); + } + + // Check for streamable HTTP transport (has url but not sse) + // Cline/Claude Desktop format: { url: "...", headers: {...} } + if (defAny.url && typeof defAny.url === "string") { + const options: any = {}; + if (defAny.headers) { + options.requestInit = { headers: defAny.headers }; + } + return new StreamableHTTPClientTransport(new URL(defAny.url), options); + } + + // Check for stdio transport (explicit type or has command) + // Cline/Claude Desktop format: { command: "...", args: [...], env: {...} } + if (explicitType === "stdio" || defAny.command) { + return new StdioClientTransport({ + command: defAny.command, + args: defAny.args, + env: { + ...(process.env as any), + ...(defAny.env ?? {}), + }, + cwd: cwd(), + }); + } + + throw new Error( + `Unsupported transport configuration: ${JSON.stringify(def)}`, + ); } async function getOrCreateMcpClient( @@ -49,47 +138,7 @@ async function getOrCreateMcpClient( return client; } - let transport: - | StdioClientTransport - | StreamableHTTPClientTransport - | SSEClientTransport; - // Runtime type guards for union shape - if ( - typeof (def as any).transportType === "string" && - (def as any).transportType === "sse" - ) { - const options: any = {}; - if ((def as any).headers) { - options.requestInit = { headers: (def as any).headers }; - options.eventSourceInit = { headers: (def as any).headers }; - } - transport = new SSEClientTransport(new URL((def as any).url), options); - } else if ("url" in (def as any) && typeof (def as any).url === "string") { - const options: any = {}; - if ((def as any).headers) { - options.requestInit = { headers: (def as any).headers }; - } - transport = new StreamableHTTPClientTransport( - new URL((def as any).url), - options, - ); - } else if ( - (typeof (def as any).transportType === "string" && - (def as any).transportType === "stdio") || - ("command" in (def as any)) - ) { - transport = new StdioClientTransport({ - command: (def as any).command, - args: (def as any).args, - env: { - ...(process.env as any), - ...((def as any).env ?? {}), - }, - cwd: cwd(), - }); - } else { - throw new Error(`Unsupported transport type: ${JSON.stringify(def)}`); - } + const transport = createTransport(def); const connecting = (async () => { const client = new Client({ diff --git a/packages/mcp-sampling-ai-provider/deno.json b/packages/mcp-sampling-ai-provider/deno.json index 80dcd1d..5003e86 100644 --- a/packages/mcp-sampling-ai-provider/deno.json +++ b/packages/mcp-sampling-ai-provider/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/mcp-sampling-ai-provider", - "version": "0.1.8", + "version": "0.1.9", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" diff --git a/packages/utils/deno.json b/packages/utils/deno.json index 17eda89..fbde954 100644 --- a/packages/utils/deno.json +++ b/packages/utils/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/utils", - "version": "0.2.3", + "version": "0.2.4", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git"