# MCP integration (/docs/mcp-integration)



This page is the consumer-side pattern for wrapping MCP server calls
as `@pleach/core` tools — the integration you write yourself today,
using `defineTool` over `@modelcontextprotocol/sdk`. For the
`@pleach/mcp` package itself — the SKU that projects a runtime's tool
registry onto the MCP wire — see [`@pleach/mcp`](/docs/mcp).

`@pleach/core` integrates with the Model Context Protocol in
both directions: exposing its own tool registry as an MCP server,
and consuming tools from external MCP servers through a
`defineTool` wrapper. The dedicated `@pleach/mcp` SKU collapses the
server-side projection into a typed package; the consumer-side
wrapper documented here remains the supported path for pulling
external MCP tools into a runtime.

The Model Context Protocol is a standard for exposing tools,
resources, and prompts to AI agents. MCP servers expose
capabilities (filesystem access, GitHub queries, database
introspection); MCP clients connect and call them.

The two integration directions are independent:

| Direction                                                                    | What ships in `@pleach/core` today                                                                                                                       |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pleach **exposes** its tools to an external MCP client (Claude Code, an IDE) | `@pleach/core` root barrel — `createHarnessMCPServer`, `createMCPSDKServer`                                                                              |
| Pleach **consumes** tools from an external MCP server                        | Hand-rolled `defineTool` wrapper over `@modelcontextprotocol/sdk`. The dedicated `@pleach/mcp` SKU is reserved on npm and will collapse the boilerplate. |

This page documents both directions.

```typescript
import { defineTool } from "@pleach/core";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
```

<SourceMeta source="{ label: &#x22;src/mcp/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/mcp&#x22; }" />

## Pleach as MCP server (`@pleach/core`) [#pleach-as-mcp-server-pleachcore]

`createHarnessMCPServer` builds a typed `MCPServer` from a
`ToolRegistryWrapper`. The in-process projection — calling
`listTools` / `callTool` on the returned object directly — is the
stable, recommended path and is what the substrate's own
integration tests exercise. Wiring it onto a live stdio transport
so external MCP clients can call your registered tools is the
early-stage (beta) arm; lock against the in-process shape first
and adopt the stdio wiring as it matures.

The MCP server symbols ship from the `@pleach/core` root barrel —
there is no `@pleach/core/mcp` subpath:

```typescript
import {
  createHarnessMCPServer,
  createMCPSDKServer,
  ToolRegistryWrapper,
} from "@pleach/core";

const server = createHarnessMCPServer({
  registry:     new ToolRegistryWrapper(),
  name:         "my-pleach-host",
  version:      "1.0.0",
  toolFilter:   (tool) => tool.category !== "internal",
  toolExecutor: async (name, args) => myExecutor(name, args),
  maxTools:     100,
});

// Or wire through the official SDK:
const sdkServer = await createMCPSDKServer({ registry, toolExecutor });
```

`HarnessMCPServerOptions` carries the registry, an optional
filter, the executor (`(name, args) => Promise<ToolResult>`), and
a `maxTools` cap. Without `toolExecutor`, `callTool` reports
"No tool executor configured" — wire it explicitly.

The returned `MCPServer` exposes:

```typescript
interface MCPServer {
  name:         string;
  version:      string;
  capabilities: { tools: boolean; resources: boolean };
  listTools:    () => Promise<MCPTool[]>;
  callTool:     (name: string, args: Record<string, unknown>) => Promise<ToolResult>;
  listResources: () => Promise<MCPResource[]>;
  readResource:  (uri: string) => Promise<{ contents: string; mimeType: string }>;
}
```

`generateToolsMarkdown(registry)` and
`generateToolsManifest(registry)` ship as helpers for static
exports of the catalog.

## Pleach as MCP client [#pleach-as-mcp-client]

An MCP server exposes three resource kinds:

| Kind      | What it carries                                | Pleach equivalent   |
| --------- | ---------------------------------------------- | ------------------- |
| Tools     | Named functions with JSON-schema args + return | `defineTool`        |
| Resources | Read-only data (files, URIs)                   | Tool that fetches   |
| Prompts   | Reusable prompt templates                      | Prompt contribution |

The substrate's primitives map cleanly. The integration is one
translation layer.

## Pattern: wrap an MCP server as `defineTool` calls [#pattern-wrap-an-mcp-server-as-definetool-calls]

A function that introspects an MCP server's tool list and
generates a `ToolDefinition` for each:

```typescript
// lib/mcp/mcpTools.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { defineTool, type ToolDefinition } from "@pleach/core";
import { z } from "zod";

export async function mcpTools(opts: {
  serverCommand: string;
  serverArgs?:   string[];
  toolPrefix?:   string;
}): Promise<ToolDefinition[]> {
  const transport = new StdioClientTransport({
    command: opts.serverCommand,
    args:    opts.serverArgs ?? [],
  });

  const client = new Client(
    { name: "@pleach/core-consumer", version: "1.0.0" },
    { capabilities: {} },
  );

  await client.connect(transport);

  const { tools } = await client.listTools();

  return tools.map((mcpTool) =>
    defineTool({
      name: `${opts.toolPrefix ?? "mcp_"}${mcpTool.name}`,
      description: mcpTool.description ?? "(MCP-sourced tool)",
      // Convert JSON Schema → Zod. Use a small adapter or the
      // `zod-from-json-schema` library; pass-through for unknown
      // shapes (the MCP server validates server-side anyway).
      inputSchema: zodFromJsonSchema(mcpTool.inputSchema) as z.ZodType<unknown>,
      async execute(input, ctx) {
        const result = await client.callTool({
          name:      mcpTool.name,
          arguments: input as Record<string, unknown>,
        });
        if (result.isError) {
          throw new Error(`MCP tool ${mcpTool.name} failed: ${JSON.stringify(result.content)}`);
        }
        return result.content;
      },
    }),
  );
}
```

Register the proxied tools with the runtime:

```typescript
const mcpToolDefs = await mcpTools({
  serverCommand: "uvx",
  serverArgs:    ["mcp-server-filesystem", "--root", "/data"],
  toolPrefix:    "fs_",
});

setOrchestratorRegistry({ tools: mcpToolDefs });

const runtime = new SessionRuntime({
  /* ... */
});

const session = await runtime.createSession({
  tools: { enabled: mcpToolDefs.map((t) => t.name) },
});
```

The MCP server runs as a subprocess; the client talks to it over
stdio. The runtime sees ordinary `defineTool`-shaped tools.

### What the wrapper handles [#what-the-wrapper-handles]

* **Schema translation.** MCP servers publish JSON Schema; the
  runtime expects Zod. A small adapter library handles the
  common cases; complex schemas can fall back to
  `z.unknown()` with server-side validation.
* **Transport.** Stdio is the simplest; HTTP / SSE transports
  are also supported by the MCP SDK.
* **Error propagation.** MCP errors become tool failures —
  surfacing as `tool.failed` stream events with the structured
  error.
* **Lifecycle.** The client connection is per-runtime. Close it
  in your cleanup path; the substrate doesn't manage subprocess
  lifecycles.

### What the wrapper doesn't handle [#what-the-wrapper-doesnt-handle]

* **Connection pooling.** Multiple runtimes sharing one MCP
  server need a shared client. Wrap the client construction in
  a singleton.
* **Resource subscriptions.** The MCP `subscribe`/`unsubscribe`
  surface for resource updates doesn't map cleanly to
  `defineTool`. Use the lower-level client directly for those.
* **Server-pushed prompts.** MCP server-provided prompts could
  be threaded as static prompt contributions; the wrapper above
  doesn't do that automatically.

## Pattern: MCP as a prompt source [#pattern-mcp-as-a-prompt-source]

For MCP servers that publish prompts, register them as
`PromptContribution` entries:

```typescript
import type { HarnessPlugin } from "@pleach/core";

async function mcpPromptPlugin(client: Client): Promise<HarnessPlugin> {
  const { prompts } = await client.listPrompts();

  return {
    name: "mcp-prompts",

    contributePrompts: () =>
      prompts.map((mcpPrompt) => ({
        id:      `mcp-prompts.${mcpPrompt.name}`,
        mode:    "append" as const,
        scope:   undefined,
        content: async (ctx) => {
          const resolved = await client.getPrompt({
            name:      mcpPrompt.name,
            arguments: { callClass: ctx.callClass },
          });
          return resolved.messages.map((m) =>
            typeof m.content === "string" ? m.content : m.content.text,
          ).join("\n\n");
        },
      })),
  };
}
```

The `content` function runs at prompt-composition time, so it
re-fetches per call. For static prompts, cache the result at
plugin construction.

The runtime-aware contribution hook is the right home for
prompts that depend on session state — MCP doesn't model that
directly, but a wrapper plugin can.

## Pattern: MCP resources as retrieval [#pattern-mcp-resources-as-retrieval]

For MCP servers that publish resources (files, URIs, database
schemas), the cleanest integration is a single retrieval tool
that proxies to `resources/list` + `resources/read`:

```typescript
// lib/mcp/mcpRetrievalTool.ts
import { defineTool } from "@pleach/core";

export function mcpRetrievalTool(client: Client) {
  return defineTool({
    name: "mcp_retrieve",
    description: "Retrieve a resource from the MCP server by URI.",
    inputSchema: z.object({
      uri:   z.string(),
      limit: z.number().int().optional(),
    }),
    async execute(input, ctx) {
      const result = await client.readResource({ uri: input.uri });
      return { contents: result.contents };
    },
  });
}
```

For more sophisticated retrieval (RAG over MCP resources), pair
this with a vector store — embed the resource list, retrieve top-k
URIs, and read them via the tool.

## Connecting to a remote MCP server [#connecting-to-a-remote-mcp-server]

The MCP SDK supports HTTP and SSE transports as well as stdio.
For a remote MCP server:

```typescript
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

const transport = new SSEClientTransport(
  new URL("https://mcp.example.com/sse"),
  { /* auth headers */ },
);

const client = new Client(/* ... */);
await client.connect(transport);
```

For production deployments, the SSE transport pairs naturally
with the substrate's own SSE wire — the same long-lived stream
shape on both ends.

## Health checks [#health-checks]

MCP servers can fail (subprocess died, network blip, auth
expired). Add a periodic ping to the connection management
layer:

```typescript
async function pingMcp(client: Client): Promise<boolean> {
  try {
    await client.ping();
    return true;
  } catch {
    return false;
  }
}

setInterval(async () => {
  if (!(await pingMcp(client))) {
    log.warn("MCP server unreachable; reconnecting");
    await reconnect();
  }
}, 30_000);
```

Surface the MCP server's health through the substrate's
`/api/harness/health` route by adding a custom component check.

## What `@pleach/mcp` adds today [#what-pleachmcp-adds-today]

`@pleach/mcp@0.1.0` bundles the server primitive (`MCPServer`),
the harness-tools-to-MCP projection (`adaptHarnessTools`,
`projectToolDefinitionToMCP`), and the stdio transport end-to-end.
The SSE and WebSocket arms ship in the transport union and on
`MCPServerStartOptions` so consumer code can lock against the
final shape today; `server.start({ transport: "sse" })` and
`"websocket"` throw `NotImplementedError("D-PA-181")` until the
next slice. `registerSession()` throws
`NotImplementedError("D-PA-184")` until `@pleach/gateway` C3
multi-tenant routing lands — see [`@pleach/mcp`](/docs/mcp) for
the per-arm status table.

To serve over stdio today, hand the pluggable transport to
`start()`. `createPluggableStdioTransport()` owns the JSON-RPC
`initialize` handshake natively — prefer it over the legacy
`start({ transport: "stdio" })` options-bag form, which builds the
handlers but does not bind a transport:

```typescript
import {
  MCPServer,
  createPluggableStdioTransport,
  type HarnessToolExecutor,
} from "@pleach/mcp";

const executor: HarnessToolExecutor = async (name, args) => {
  // Bind `name` to a dispatch path of your choice.
  return { success: true, output: { name, args } };
};

const server = new MCPServer(
  { name: "my-mcp-server", version: "1.0.0", runtime, autoAdaptTools: true },
  executor,
);

await server.start(createPluggableStdioTransport());
// Server now answers tools/list + tools/call on stdio.
```

For the consumer side (running the runtime as an MCP client into
other servers), the hand-rolled wrapper patterns above remain the
supported path — `@pleach/mcp` Phase A focuses on the
runtime-as-server projection. The substrate's own integration
tests still use the hand-rolled patterns above against reference
MCP servers.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="@pleach/mcp" href="/docs/mcp" description="The SKU reference — server-side projection of a runtime's tool registry onto the MCP wire." />

  <Card title="Tools" href="/docs/tools" description="The `defineTool` contract the MCP wrapper builds on." />

  <Card title="Prompts" href="/docs/prompts" description="`PromptContribution` for MCP prompt sources." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="Wrapping MCP integration as a `HarnessPlugin`." />
</Cards>
