@crewhaus/mcp-server 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +42 -0
- package/dist/build.js +114 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +110 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.js +41 -0
- package/package.json +41 -0
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registration + SDK-server construction for the MCP projection.
|
|
3
|
+
*
|
|
4
|
+
* `buildConfiguredServer` mints a fresh `McpServer` with the projected tools
|
|
5
|
+
* registered; the stdio path calls it once, the SSE path calls it per session.
|
|
6
|
+
* Every tool delegates to the injected `invoke`, which is the ONLY behavioural
|
|
7
|
+
* dependency — there is no compiler/runtime coupling here.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { type CreateMcpServerOptions, type McpSubAgentDescriptor, type McpToolsMode } from "./types.js";
|
|
11
|
+
/** Resolve the tools-mode default (`"chat"`), mirroring the IR lower step. */
|
|
12
|
+
export declare function resolveToolsMode(mode: McpToolsMode | undefined): McpToolsMode;
|
|
13
|
+
/**
|
|
14
|
+
* Reject a projection that cannot be built. The spec's cross-field check
|
|
15
|
+
* already guards `per-subagent` upstream, but this package must be safe when
|
|
16
|
+
* driven standalone (e.g. from a hand-built `invoke`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateOptions(opts: CreateMcpServerOptions): void;
|
|
19
|
+
interface ResolvedSubAgentTool {
|
|
20
|
+
/** The (sanitized, de-duplicated) MCP tool name. */
|
|
21
|
+
readonly toolName: string;
|
|
22
|
+
/** The ORIGINAL spec sub-agent name, threaded back through `McpInvokeContext`. */
|
|
23
|
+
readonly subAgent: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Map sub-agent descriptors to MCP tools, sanitizing names and de-duplicating
|
|
28
|
+
* against each other AND the reserved `chat` tool. The original sub-agent name
|
|
29
|
+
* is preserved on `subAgent` so `invoke` still routes by the real name even
|
|
30
|
+
* when the tool name was rewritten.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveSubAgentTools(subAgents: readonly McpSubAgentDescriptor[]): readonly ResolvedSubAgentTool[];
|
|
33
|
+
/** The tool names a projection will register, without building a server. */
|
|
34
|
+
export declare function computeToolNames(opts: CreateMcpServerOptions): readonly string[];
|
|
35
|
+
/** Register the projected tools onto `server`; returns their names in order. */
|
|
36
|
+
export declare function registerAgentTools(server: McpServer, opts: CreateMcpServerOptions): readonly string[];
|
|
37
|
+
/** Mint a fresh SDK server with the projected tools registered. */
|
|
38
|
+
export declare function buildConfiguredServer(opts: CreateMcpServerOptions): {
|
|
39
|
+
readonly server: McpServer;
|
|
40
|
+
readonly toolNames: readonly string[];
|
|
41
|
+
};
|
|
42
|
+
export {};
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registration + SDK-server construction for the MCP projection.
|
|
3
|
+
*
|
|
4
|
+
* `buildConfiguredServer` mints a fresh `McpServer` with the projected tools
|
|
5
|
+
* registered; the stdio path calls it once, the SSE path calls it per session.
|
|
6
|
+
* Every tool delegates to the injected `invoke`, which is the ONLY behavioural
|
|
7
|
+
* dependency — there is no compiler/runtime coupling here.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { CHAT_TOOL_NAME, McpServerError, } from "./types.js";
|
|
12
|
+
const DEFAULT_NAME = "crewhaus";
|
|
13
|
+
const DEFAULT_VERSION = "0.0.0";
|
|
14
|
+
/** Resolve the tools-mode default (`"chat"`), mirroring the IR lower step. */
|
|
15
|
+
export function resolveToolsMode(mode) {
|
|
16
|
+
return mode ?? "chat";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reject a projection that cannot be built. The spec's cross-field check
|
|
20
|
+
* already guards `per-subagent` upstream, but this package must be safe when
|
|
21
|
+
* driven standalone (e.g. from a hand-built `invoke`).
|
|
22
|
+
*/
|
|
23
|
+
export function validateOptions(opts) {
|
|
24
|
+
if (typeof opts.invoke !== "function") {
|
|
25
|
+
throw new McpServerError("createMcpServer requires an `invoke` function");
|
|
26
|
+
}
|
|
27
|
+
if (opts.transport !== "stdio" && opts.transport !== "sse") {
|
|
28
|
+
throw new McpServerError(`unsupported MCP transport ${JSON.stringify(opts.transport)} (expected "stdio" or "sse")`);
|
|
29
|
+
}
|
|
30
|
+
if (resolveToolsMode(opts.tools) === "per-subagent" &&
|
|
31
|
+
(opts.subAgents === undefined || opts.subAgents.length === 0)) {
|
|
32
|
+
throw new McpServerError('tools: "per-subagent" requires at least one sub-agent to project');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Fold a spec name into an MCP-safe tool name (`[A-Za-z0-9_-]`). */
|
|
36
|
+
function sanitizeToolName(name) {
|
|
37
|
+
const cleaned = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
38
|
+
return cleaned.length > 0 ? cleaned : "subagent";
|
|
39
|
+
}
|
|
40
|
+
/** Make `base` unique against `used`, appending `_2`, `_3`, … on collision. */
|
|
41
|
+
function uniqueName(base, used) {
|
|
42
|
+
if (!used.has(base)) {
|
|
43
|
+
used.add(base);
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
let n = 2;
|
|
47
|
+
while (used.has(`${base}_${n}`))
|
|
48
|
+
n += 1;
|
|
49
|
+
const candidate = `${base}_${n}`;
|
|
50
|
+
used.add(candidate);
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Map sub-agent descriptors to MCP tools, sanitizing names and de-duplicating
|
|
55
|
+
* against each other AND the reserved `chat` tool. The original sub-agent name
|
|
56
|
+
* is preserved on `subAgent` so `invoke` still routes by the real name even
|
|
57
|
+
* when the tool name was rewritten.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveSubAgentTools(subAgents) {
|
|
60
|
+
const used = new Set([CHAT_TOOL_NAME]);
|
|
61
|
+
return subAgents.map((sa) => ({
|
|
62
|
+
toolName: uniqueName(sanitizeToolName(sa.name), used),
|
|
63
|
+
subAgent: sa.name,
|
|
64
|
+
description: sa.description,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
/** The tool names a projection will register, without building a server. */
|
|
68
|
+
export function computeToolNames(opts) {
|
|
69
|
+
if (resolveToolsMode(opts.tools) === "chat")
|
|
70
|
+
return [CHAT_TOOL_NAME];
|
|
71
|
+
return [CHAT_TOOL_NAME, ...resolveSubAgentTools(opts.subAgents ?? []).map((t) => t.toolName)];
|
|
72
|
+
}
|
|
73
|
+
/** Run `invoke`, mapping success/failure onto a `CallToolResult` (never throws). */
|
|
74
|
+
async function runInvoke(invoke, message, context) {
|
|
75
|
+
try {
|
|
76
|
+
const text = await invoke(message, context);
|
|
77
|
+
return { content: [{ type: "text", text: typeof text === "string" ? text : String(text) }] };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
81
|
+
return { isError: true, content: [{ type: "text", text: `invoke failed: ${reason}` }] };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Register the projected tools onto `server`; returns their names in order. */
|
|
85
|
+
export function registerAgentTools(server, opts) {
|
|
86
|
+
const agentName = opts.name ?? DEFAULT_NAME;
|
|
87
|
+
const names = [];
|
|
88
|
+
server.registerTool(CHAT_TOOL_NAME, {
|
|
89
|
+
title: `Chat with ${agentName}`,
|
|
90
|
+
description: opts.chatToolDescription ??
|
|
91
|
+
`Send a message to the ${agentName} agent and receive its final response.`,
|
|
92
|
+
inputSchema: { message: z.string().describe("The message to send to the agent.") },
|
|
93
|
+
}, ({ message }) => runInvoke(opts.invoke, message, { toolName: CHAT_TOOL_NAME }));
|
|
94
|
+
names.push(CHAT_TOOL_NAME);
|
|
95
|
+
if (resolveToolsMode(opts.tools) === "per-subagent") {
|
|
96
|
+
for (const tool of resolveSubAgentTools(opts.subAgents ?? [])) {
|
|
97
|
+
server.registerTool(tool.toolName, {
|
|
98
|
+
title: `Delegate to ${tool.subAgent}`,
|
|
99
|
+
description: tool.description,
|
|
100
|
+
inputSchema: {
|
|
101
|
+
message: z.string().describe(`The message to route to the ${tool.subAgent} sub-agent.`),
|
|
102
|
+
},
|
|
103
|
+
}, ({ message }) => runInvoke(opts.invoke, message, { toolName: tool.toolName, subAgent: tool.subAgent }));
|
|
104
|
+
names.push(tool.toolName);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return names;
|
|
108
|
+
}
|
|
109
|
+
/** Mint a fresh SDK server with the projected tools registered. */
|
|
110
|
+
export function buildConfiguredServer(opts) {
|
|
111
|
+
const server = new McpServer({ name: opts.name ?? DEFAULT_NAME, version: opts.version ?? DEFAULT_VERSION }, opts.instructions === undefined ? undefined : { instructions: opts.instructions });
|
|
112
|
+
const toolNames = registerAgentTools(server, opts);
|
|
113
|
+
return { server, toolNames };
|
|
114
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crewhaus/mcp-server` — project a compiled bundle's turn function as an MCP
|
|
3
|
+
* server (Loop-contract 0.4, Item 1 / G30). See `./types.ts` for the design
|
|
4
|
+
* rationale and the `IrExpose`/`IrExposeMcp` correspondence.
|
|
5
|
+
*
|
|
6
|
+
* `createMcpServer({ invoke, transport, tools, subAgents })` registers a `chat`
|
|
7
|
+
* tool (and, under `tools: "per-subagent"`, one tool per sub-agent) that all
|
|
8
|
+
* delegate to the injected `invoke`, then binds the requested transport:
|
|
9
|
+
* - `stdio` → a `StdioMcpServer` you `listen()` on the process stdio.
|
|
10
|
+
* - `sse` → an `SseMcpServer` whose `fetch(Request)` handler mounts in any
|
|
11
|
+
* Web-Standard `fetch` pipeline (Bun / Workers / gateway-server).
|
|
12
|
+
*/
|
|
13
|
+
import { type CreateMcpServerOptions, type McpServerHandle } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Build an MCP server that projects `invoke` as tools over the chosen transport.
|
|
16
|
+
* Throws {@link McpServerError} on a misconfigured projection. Narrow the return
|
|
17
|
+
* on `.transport` to reach the transport-specific methods.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMcpServer(opts: CreateMcpServerOptions): McpServerHandle;
|
|
20
|
+
export { CHAT_TOOL_NAME, McpServerError } from "./types.js";
|
|
21
|
+
export type { CreateMcpServerOptions, McpInvoke, McpInvokeContext, McpServerHandle, McpSubAgentDescriptor, McpToolsMode, McpTransportKind, SseMcpServer, StdioMcpServer, StdioStreams, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crewhaus/mcp-server` — project a compiled bundle's turn function as an MCP
|
|
3
|
+
* server (Loop-contract 0.4, Item 1 / G30). See `./types.ts` for the design
|
|
4
|
+
* rationale and the `IrExpose`/`IrExposeMcp` correspondence.
|
|
5
|
+
*
|
|
6
|
+
* `createMcpServer({ invoke, transport, tools, subAgents })` registers a `chat`
|
|
7
|
+
* tool (and, under `tools: "per-subagent"`, one tool per sub-agent) that all
|
|
8
|
+
* delegate to the injected `invoke`, then binds the requested transport:
|
|
9
|
+
* - `stdio` → a `StdioMcpServer` you `listen()` on the process stdio.
|
|
10
|
+
* - `sse` → an `SseMcpServer` whose `fetch(Request)` handler mounts in any
|
|
11
|
+
* Web-Standard `fetch` pipeline (Bun / Workers / gateway-server).
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
16
|
+
import { buildConfiguredServer, computeToolNames, validateOptions } from "./build.js";
|
|
17
|
+
import { McpServerError, } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Build an MCP server that projects `invoke` as tools over the chosen transport.
|
|
20
|
+
* Throws {@link McpServerError} on a misconfigured projection. Narrow the return
|
|
21
|
+
* on `.transport` to reach the transport-specific methods.
|
|
22
|
+
*/
|
|
23
|
+
export function createMcpServer(opts) {
|
|
24
|
+
validateOptions(opts);
|
|
25
|
+
return opts.transport === "stdio" ? createStdioServer(opts) : createSseServer(opts);
|
|
26
|
+
}
|
|
27
|
+
function createStdioServer(opts) {
|
|
28
|
+
const { server, toolNames } = buildConfiguredServer(opts);
|
|
29
|
+
let connected = false;
|
|
30
|
+
const connect = async (transport) => {
|
|
31
|
+
if (connected) {
|
|
32
|
+
throw new McpServerError("this MCP server is already connected to a transport");
|
|
33
|
+
}
|
|
34
|
+
connected = true;
|
|
35
|
+
try {
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
connected = false;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const listen = (streams) => connect(new StdioServerTransport(streams?.stdin, streams?.stdout));
|
|
44
|
+
const close = async () => {
|
|
45
|
+
await server.close();
|
|
46
|
+
connected = false;
|
|
47
|
+
};
|
|
48
|
+
return { transport: "stdio", server, toolNames, connect, listen, close };
|
|
49
|
+
}
|
|
50
|
+
function createSseServer(opts) {
|
|
51
|
+
const toolNames = computeToolNames(opts);
|
|
52
|
+
const sessions = new Map();
|
|
53
|
+
let closed = false;
|
|
54
|
+
const closeSession = async (id) => {
|
|
55
|
+
const session = sessions.get(id);
|
|
56
|
+
if (session === undefined)
|
|
57
|
+
return;
|
|
58
|
+
sessions.delete(id);
|
|
59
|
+
await session.transport.close().catch(() => { });
|
|
60
|
+
await session.server.close().catch(() => { });
|
|
61
|
+
};
|
|
62
|
+
const openSession = async (request) => {
|
|
63
|
+
const { server } = buildConfiguredServer(opts);
|
|
64
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
65
|
+
sessionIdGenerator: () => randomUUID(),
|
|
66
|
+
onsessioninitialized: (id) => {
|
|
67
|
+
sessions.set(id, { server, transport });
|
|
68
|
+
},
|
|
69
|
+
onsessionclosed: (id) => {
|
|
70
|
+
void closeSession(id);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
await server.connect(transport);
|
|
74
|
+
const response = await transport.handleRequest(request);
|
|
75
|
+
// A request that never initialized a session (e.g. an invalid non-initialize
|
|
76
|
+
// POST with no session id) leaves an orphan transport — tear it down so a
|
|
77
|
+
// misbehaving client cannot leak servers.
|
|
78
|
+
const id = transport.sessionId;
|
|
79
|
+
if (id === undefined || !sessions.has(id)) {
|
|
80
|
+
await transport.close().catch(() => { });
|
|
81
|
+
await server.close().catch(() => { });
|
|
82
|
+
}
|
|
83
|
+
return response;
|
|
84
|
+
};
|
|
85
|
+
const fetch = async (request) => {
|
|
86
|
+
if (closed)
|
|
87
|
+
return jsonRpcErrorResponse(503, -32000, "MCP server is closed");
|
|
88
|
+
const sessionId = request.headers.get("mcp-session-id") ?? undefined;
|
|
89
|
+
if (sessionId !== undefined) {
|
|
90
|
+
const session = sessions.get(sessionId);
|
|
91
|
+
if (session !== undefined)
|
|
92
|
+
return session.transport.handleRequest(request);
|
|
93
|
+
return jsonRpcErrorResponse(404, -32001, "MCP session not found");
|
|
94
|
+
}
|
|
95
|
+
return openSession(request);
|
|
96
|
+
};
|
|
97
|
+
const close = async () => {
|
|
98
|
+
closed = true;
|
|
99
|
+
await Promise.all([...sessions.keys()].map((id) => closeSession(id)));
|
|
100
|
+
};
|
|
101
|
+
return { transport: "sse", toolNames, fetch, close };
|
|
102
|
+
}
|
|
103
|
+
/** A minimal JSON-RPC error envelope for transport-level rejections. */
|
|
104
|
+
function jsonRpcErrorResponse(status, code, message) {
|
|
105
|
+
return new Response(JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }), {
|
|
106
|
+
status,
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export { CHAT_TOOL_NAME, McpServerError } from "./types.js";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R5 (`mcp-server`) — public surface for the MCP-server PROJECTION
|
|
3
|
+
* (Loop-contract 0.4, Item 1 / G30).
|
|
4
|
+
*
|
|
5
|
+
* The `expose:` spec block lowers to `IrExpose`/`IrExposeMcp` (`@crewhaus/ir`)
|
|
6
|
+
* and asks the compiler to project THIS compiled bundle's turn function as an
|
|
7
|
+
* MCP server so Claude Code / IDEs / other CrewHaus runtimes can call the whole
|
|
8
|
+
* agent as a tool. This package is the runtime half of that projection.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately IR- and runtime-agnostic: the bundle's turn function is INJECTED
|
|
11
|
+
* as `invoke`, so `mcp-server` depends on neither `@crewhaus/compiler` nor
|
|
12
|
+
* `@crewhaus/runtime-core`. The CLI slice builds `invoke` from a compiled
|
|
13
|
+
* bundle and hands it here; `mcp-server` only knows how to wrap it in the
|
|
14
|
+
* official `@modelcontextprotocol/sdk` server + a transport.
|
|
15
|
+
*
|
|
16
|
+
* - `transport: "stdio"` → a spawned stdio MCP server (the
|
|
17
|
+
* `crewhaus serve --mcp` path). Node/Bun only.
|
|
18
|
+
* - `transport: "sse"` → a Web-Standard `fetch(Request): Promise<Response>`
|
|
19
|
+
* handler that SSE-streams responses (built on the SDK's Streamable-HTTP
|
|
20
|
+
* transport). Mountable in a `Bun.serve` / Cloudflare Workers /
|
|
21
|
+
* `gateway-server` fetch pipeline so the SSE exposure can ride the
|
|
22
|
+
* gateway's tenancy/budgets where the shape has them.
|
|
23
|
+
*
|
|
24
|
+
* Tool projection mirrors `IrExposeMcp.tools`:
|
|
25
|
+
* - `"chat"` (default) → one primary `chat` tool taking `{ message }` and
|
|
26
|
+
* returning the final assistant text.
|
|
27
|
+
* - `"per-subagent"` → the `chat` tool PLUS one tool per declared sub-agent,
|
|
28
|
+
* each delegating to `invoke` with the sub-agent's name in the call
|
|
29
|
+
* context so the injected fn can route to that sub-agent.
|
|
30
|
+
*/
|
|
31
|
+
import type { Readable, Writable } from "node:stream";
|
|
32
|
+
import { McpError } from "@crewhaus/errors";
|
|
33
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
34
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
35
|
+
/** Wire transport for the projected MCP server. Mirrors `IrExposeMcp.transport`. */
|
|
36
|
+
export type McpTransportKind = "stdio" | "sse";
|
|
37
|
+
/** Which tools to project. Mirrors the RESOLVED `IrExposeMcp.tools`. */
|
|
38
|
+
export type McpToolsMode = "chat" | "per-subagent";
|
|
39
|
+
/** Name of the primary invoke tool that delegates the whole agent turn. */
|
|
40
|
+
export declare const CHAT_TOOL_NAME = "chat";
|
|
41
|
+
/**
|
|
42
|
+
* Context handed to the injected `invoke` on every MCP tool call.
|
|
43
|
+
*
|
|
44
|
+
* `toolName` is the MCP tool the client called. `subAgent` is set ONLY for a
|
|
45
|
+
* per-sub-agent tool and carries the ORIGINAL spec sub-agent name (before MCP
|
|
46
|
+
* tool-name sanitization), so the injected fn can route the message to that
|
|
47
|
+
* sub-agent. Absent for the primary `chat` tool.
|
|
48
|
+
*/
|
|
49
|
+
export interface McpInvokeContext {
|
|
50
|
+
readonly toolName: string;
|
|
51
|
+
readonly subAgent?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The injected delegate: run one agent turn for `message` and resolve with the
|
|
55
|
+
* final assistant text. A plain `(message: string) => Promise<string>` is
|
|
56
|
+
* assignable here — the `context` argument is optional for callers that don't
|
|
57
|
+
* need per-sub-agent routing.
|
|
58
|
+
*/
|
|
59
|
+
export type McpInvoke = (message: string, context: McpInvokeContext) => Promise<string>;
|
|
60
|
+
/** A sub-agent to project as its own MCP tool under `tools: "per-subagent"`. */
|
|
61
|
+
export interface McpSubAgentDescriptor {
|
|
62
|
+
/** The spec sub-agent name (`sub_agents.<name>`). Passed back via `McpInvokeContext.subAgent`. */
|
|
63
|
+
readonly name: string;
|
|
64
|
+
/** Shown to the calling model as the tool's description. */
|
|
65
|
+
readonly description: string;
|
|
66
|
+
}
|
|
67
|
+
/** Options for {@link createMcpServer}. */
|
|
68
|
+
export interface CreateMcpServerOptions {
|
|
69
|
+
/** The bundle's turn function. Required. */
|
|
70
|
+
readonly invoke: McpInvoke;
|
|
71
|
+
/** Which transport to bind. Required. */
|
|
72
|
+
readonly transport: McpTransportKind;
|
|
73
|
+
/** Tool projection mode. Defaults to `"chat"`. */
|
|
74
|
+
readonly tools?: McpToolsMode;
|
|
75
|
+
/** Sub-agents to project — required (and non-empty) when `tools: "per-subagent"`. */
|
|
76
|
+
readonly subAgents?: readonly McpSubAgentDescriptor[];
|
|
77
|
+
/** MCP server info `name` advertised to clients. Defaults to `"crewhaus"`. */
|
|
78
|
+
readonly name?: string;
|
|
79
|
+
/** MCP server info `version`. Defaults to `"0.0.0"`. */
|
|
80
|
+
readonly version?: string;
|
|
81
|
+
/** Optional MCP server instructions advertised in the initialize result. */
|
|
82
|
+
readonly instructions?: string;
|
|
83
|
+
/** Override the primary `chat` tool's description. */
|
|
84
|
+
readonly chatToolDescription?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Streams for {@link StdioMcpServer.listen}. Defaults to the process's
|
|
88
|
+
* `stdin`/`stdout` when omitted; the injection hook exists mainly for tests.
|
|
89
|
+
*/
|
|
90
|
+
export interface StdioStreams {
|
|
91
|
+
readonly stdin?: Readable;
|
|
92
|
+
readonly stdout?: Writable;
|
|
93
|
+
}
|
|
94
|
+
interface McpServerHandleBase {
|
|
95
|
+
readonly transport: McpTransportKind;
|
|
96
|
+
/** The MCP tool names registered per session, in registration order. */
|
|
97
|
+
readonly toolNames: readonly string[];
|
|
98
|
+
/** Close the server and every transport/session it owns. */
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
/** Handle returned by {@link createMcpServer} for `transport: "stdio"`. */
|
|
102
|
+
export interface StdioMcpServer extends McpServerHandleBase {
|
|
103
|
+
readonly transport: "stdio";
|
|
104
|
+
/** The underlying SDK server (advanced use: notifications, custom handlers). */
|
|
105
|
+
readonly server: McpServer;
|
|
106
|
+
/**
|
|
107
|
+
* Connect the server to any SDK {@link Transport} and start serving.
|
|
108
|
+
* Transport-agnostic — `listen()` wraps this with a `StdioServerTransport`,
|
|
109
|
+
* and tests link it to an `InMemoryTransport`. Throws if already connected.
|
|
110
|
+
*/
|
|
111
|
+
connect(transport: Transport): Promise<void>;
|
|
112
|
+
/** Bind to the process's stdio (or the injected `streams`) and serve until `close()`. */
|
|
113
|
+
listen(streams?: StdioStreams): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
/** Handle returned by {@link createMcpServer} for `transport: "sse"`. */
|
|
116
|
+
export interface SseMcpServer extends McpServerHandleBase {
|
|
117
|
+
readonly transport: "sse";
|
|
118
|
+
/**
|
|
119
|
+
* Web-Standard fetch handler. Mount it directly:
|
|
120
|
+
* `Bun.serve({ fetch: handle.fetch })`, or from a Workers/gateway `fetch`.
|
|
121
|
+
* MCP sessions are managed internally (one Streamable-HTTP transport + SDK
|
|
122
|
+
* server per session, keyed by the `mcp-session-id` header).
|
|
123
|
+
*/
|
|
124
|
+
fetch(request: Request): Promise<Response>;
|
|
125
|
+
}
|
|
126
|
+
/** Discriminated handle over the two transports. Narrow on `.transport`. */
|
|
127
|
+
export type McpServerHandle = StdioMcpServer | SseMcpServer;
|
|
128
|
+
/**
|
|
129
|
+
* Raised for misconfigured projections (missing `invoke`, unknown transport,
|
|
130
|
+
* `per-subagent` with no sub-agents, double-connect). Carries the shared
|
|
131
|
+
* `"mcp"` error code via {@link McpError}.
|
|
132
|
+
*/
|
|
133
|
+
export declare class McpServerError extends McpError {
|
|
134
|
+
readonly name = "McpServerError";
|
|
135
|
+
}
|
|
136
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R5 (`mcp-server`) — public surface for the MCP-server PROJECTION
|
|
3
|
+
* (Loop-contract 0.4, Item 1 / G30).
|
|
4
|
+
*
|
|
5
|
+
* The `expose:` spec block lowers to `IrExpose`/`IrExposeMcp` (`@crewhaus/ir`)
|
|
6
|
+
* and asks the compiler to project THIS compiled bundle's turn function as an
|
|
7
|
+
* MCP server so Claude Code / IDEs / other CrewHaus runtimes can call the whole
|
|
8
|
+
* agent as a tool. This package is the runtime half of that projection.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately IR- and runtime-agnostic: the bundle's turn function is INJECTED
|
|
11
|
+
* as `invoke`, so `mcp-server` depends on neither `@crewhaus/compiler` nor
|
|
12
|
+
* `@crewhaus/runtime-core`. The CLI slice builds `invoke` from a compiled
|
|
13
|
+
* bundle and hands it here; `mcp-server` only knows how to wrap it in the
|
|
14
|
+
* official `@modelcontextprotocol/sdk` server + a transport.
|
|
15
|
+
*
|
|
16
|
+
* - `transport: "stdio"` → a spawned stdio MCP server (the
|
|
17
|
+
* `crewhaus serve --mcp` path). Node/Bun only.
|
|
18
|
+
* - `transport: "sse"` → a Web-Standard `fetch(Request): Promise<Response>`
|
|
19
|
+
* handler that SSE-streams responses (built on the SDK's Streamable-HTTP
|
|
20
|
+
* transport). Mountable in a `Bun.serve` / Cloudflare Workers /
|
|
21
|
+
* `gateway-server` fetch pipeline so the SSE exposure can ride the
|
|
22
|
+
* gateway's tenancy/budgets where the shape has them.
|
|
23
|
+
*
|
|
24
|
+
* Tool projection mirrors `IrExposeMcp.tools`:
|
|
25
|
+
* - `"chat"` (default) → one primary `chat` tool taking `{ message }` and
|
|
26
|
+
* returning the final assistant text.
|
|
27
|
+
* - `"per-subagent"` → the `chat` tool PLUS one tool per declared sub-agent,
|
|
28
|
+
* each delegating to `invoke` with the sub-agent's name in the call
|
|
29
|
+
* context so the injected fn can route to that sub-agent.
|
|
30
|
+
*/
|
|
31
|
+
import { McpError } from "@crewhaus/errors";
|
|
32
|
+
/** Name of the primary invoke tool that delegates the whole agent turn. */
|
|
33
|
+
export const CHAT_TOOL_NAME = "chat";
|
|
34
|
+
/**
|
|
35
|
+
* Raised for misconfigured projections (missing `invoke`, unknown transport,
|
|
36
|
+
* `per-subagent` with no sub-agents, double-connect). Carries the shared
|
|
37
|
+
* `"mcp"` error code via {@link McpError}.
|
|
38
|
+
*/
|
|
39
|
+
export class McpServerError extends McpError {
|
|
40
|
+
name = "McpServerError";
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/mcp-server",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Project a compiled bundle's turn function as an MCP server (stdio + SSE) — a chat/invoke tool plus optional per-sub-agent tools delegating to an injected invoke fn, built on @modelcontextprotocol/sdk",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/errors": "0.4.0",
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
|
+
"zod": "^3.23.8"
|
|
21
|
+
},
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Max Meier",
|
|
25
|
+
"email": "max@crewhaus.ai",
|
|
26
|
+
"url": "https://crewhaus.ai"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
31
|
+
"directory": "packages/mcp-server"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/mcp-server#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
41
|
+
}
|