@procedures/mcp-server 0.0.0-stage → 0.1.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +366 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +22 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +22 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +11 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +16 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/mcp/index.d.ts +6 -0
  14. package/dist/procedures/mcp/index.d.ts.map +1 -0
  15. package/dist/procedures/mcp/index.js +6 -0
  16. package/dist/procedures/mcp/index.js.map +1 -0
  17. package/dist/procedures/mcp/list-tools.d.ts +8 -0
  18. package/dist/procedures/mcp/list-tools.d.ts.map +1 -0
  19. package/dist/procedures/mcp/list-tools.js +60 -0
  20. package/dist/procedures/mcp/list-tools.js.map +1 -0
  21. package/dist/procedures/mcp/serve.d.ts +38 -0
  22. package/dist/procedures/mcp/serve.d.ts.map +1 -0
  23. package/dist/procedures/mcp/serve.js +123 -0
  24. package/dist/procedures/mcp/serve.js.map +1 -0
  25. package/dist/register.d.ts +11 -0
  26. package/dist/register.d.ts.map +1 -0
  27. package/dist/register.js +21 -0
  28. package/dist/register.js.map +1 -0
  29. package/dist/surface.d.ts +36 -0
  30. package/dist/surface.d.ts.map +1 -0
  31. package/dist/surface.js +74 -0
  32. package/dist/surface.js.map +1 -0
  33. package/dist/transport/index.d.ts +7 -0
  34. package/dist/transport/index.d.ts.map +1 -0
  35. package/dist/transport/index.js +7 -0
  36. package/dist/transport/index.js.map +1 -0
  37. package/dist/transport/mcp-transport.d.ts +94 -0
  38. package/dist/transport/mcp-transport.d.ts.map +1 -0
  39. package/dist/transport/mcp-transport.js +368 -0
  40. package/dist/transport/mcp-transport.js.map +1 -0
  41. package/dist/transport/sse.d.ts +20 -0
  42. package/dist/transport/sse.d.ts.map +1 -0
  43. package/dist/transport/sse.js +23 -0
  44. package/dist/transport/sse.js.map +1 -0
  45. package/dist/transport/stdio.d.ts +16 -0
  46. package/dist/transport/stdio.d.ts.map +1 -0
  47. package/dist/transport/stdio.js +18 -0
  48. package/dist/transport/stdio.js.map +1 -0
  49. package/dist/types.d.ts +55 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +7 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +81 -3
  54. package/src/index.ts +48 -0
  55. package/src/module.ts +18 -0
  56. package/src/procedures/mcp/index.ts +6 -0
  57. package/src/procedures/mcp/list-tools.ts +84 -0
  58. package/src/procedures/mcp/serve.ts +170 -0
  59. package/src/register.ts +24 -0
  60. package/src/surface.ts +85 -0
  61. package/src/transport/index.ts +7 -0
  62. package/src/transport/mcp-transport.ts +418 -0
  63. package/src/transport/sse.ts +29 -0
  64. package/src/transport/stdio.ts +19 -0
  65. package/src/types.ts +64 -0
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * MCP Client Package
3
+ *
4
+ * MCP server transport for the procedure system.
5
+ * Maps PROCEDURE_REGISTRY to MCP tools for use with Claude Desktop
6
+ * and other MCP-compatible clients.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ // Importing the package registers its procedures, as in the other client packages (BUGS-2026-07 H18)
12
+ import "./register.js";
13
+
14
+ // Transport
15
+ export { McpServerTransport } from "./transport/mcp-transport.js";
16
+ export { createStdioTransport } from "./transport/stdio.js";
17
+ export { createSseTransport } from "./transport/sse.js";
18
+
19
+ // Types
20
+ export type {
21
+ McpServerTransportOptions,
22
+ McpServerTransportState,
23
+ SseTransportOptions,
24
+ } from "./types.js";
25
+
26
+ // Procedures
27
+ export { mcpServeProcedure, prepareServe, DEFAULT_SERVE_BUNDLE } from "./procedures/mcp/serve.js";
28
+ export type { ServeOptions, ServeSetup } from "./procedures/mcp/serve.js";
29
+ export { exposeRule, mergeSurfaces, importBundles, resolveBundleRegister } from "./surface.js";
30
+ export type { ToolSurface, BundleSurfaceModule } from "./surface.js";
31
+ export { mcpListToolsProcedure } from "./procedures/mcp/list-tools.js";
32
+
33
+ // Re-export core MCP types for convenience
34
+ export type {
35
+ McpTool,
36
+ McpToolDefinition,
37
+ McpToolFilter,
38
+ McpServerInfo,
39
+ McpTransportType,
40
+ } from "@procedures/mcp";
41
+
42
+ // Re-export mapping utilities
43
+ export {
44
+ proceduresToMcpTools,
45
+ procedureToMcpTool,
46
+ encodePath,
47
+ decodePath,
48
+ } from "@procedures/mcp";
package/src/module.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Module (no side effects): the MCP procedures as data, for `createRegistry()`.
3
+ */
4
+
5
+ import { defineModule, type AnyProcedure, type ProcedureModule } from "@procedures/client";
6
+ import { mcpServeProcedure } from "./procedures/mcp/serve.js";
7
+ import { mcpListToolsProcedure } from "./procedures/mcp/list-tools.js";
8
+
9
+ /**
10
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
11
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
12
+ */
13
+ export const mcpModule: ProcedureModule = defineModule("@procedures/mcp-server", [
14
+ mcpServeProcedure as AnyProcedure,
15
+ mcpListToolsProcedure as AnyProcedure,
16
+ ]);
17
+
18
+ export default mcpModule;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MCP Procedures
3
+ */
4
+
5
+ export { mcpServeProcedure } from "./serve.js";
6
+ export { mcpListToolsProcedure } from "./list-tools.js";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * mcp.list-tools Procedure
3
+ *
4
+ * List available MCP tools from the procedure registry.
5
+ */
6
+
7
+ import { z } from "zod";
8
+ import { defineProcedure, PROCEDURE_REGISTRY, type AnyProcedure } from "@procedures/client";
9
+ import { proceduresToMcpTools, type McpToolFilter } from "@procedures/mcp";
10
+
11
+ const McpListToolsInputSchema = z.object({
12
+ /** Only include procedures with these tags */
13
+ includeTags: z.array(z.string()).optional().describe("Only include procedures with these tags"),
14
+ /** Exclude procedures with these tags */
15
+ excludeTags: z.array(z.string()).optional().describe("Exclude procedures with these tags"),
16
+ /** Exclude internal procedures (default: true) */
17
+ excludeInternal: z.boolean().default(true).describe("Exclude internal procedures (default: true)"),
18
+ /** Only include procedures under this path prefix */
19
+ pathPrefix: z.array(z.string()).optional().describe("Only include procedures under this path prefix"),
20
+ });
21
+
22
+ type McpListToolsInput = z.infer<typeof McpListToolsInputSchema>;
23
+
24
+ interface McpToolInfo {
25
+ name: string;
26
+ description: string | undefined;
27
+ path: string[];
28
+ streaming: boolean | undefined;
29
+ tags: string[] | undefined;
30
+ }
31
+
32
+ interface McpListToolsOutput {
33
+ tools: McpToolInfo[];
34
+ count: number;
35
+ }
36
+
37
+ const McpListToolsOutputSchema: {
38
+ parse: (data: unknown) => McpListToolsOutput;
39
+ safeParse: (data: unknown) => { success: true; data: McpListToolsOutput };
40
+ } = {
41
+ parse: (data: unknown) => data as McpListToolsOutput,
42
+ safeParse: (data: unknown) => ({ success: true as const, data: data as McpListToolsOutput }),
43
+ };
44
+
45
+ export const mcpListToolsProcedure: AnyProcedure = defineProcedure({
46
+ path: ["mcp", "list-tools"],
47
+ input: McpListToolsInputSchema,
48
+ output: McpListToolsOutputSchema,
49
+ metadata: {
50
+ description: "List available MCP tools from the procedure registry",
51
+ tags: ["mcp", "introspection"],
52
+ },
53
+ handler: async (input: McpListToolsInput): Promise<McpListToolsOutput> => {
54
+ // Build filter, only adding defined values
55
+ const filter: McpToolFilter = {
56
+ excludeInternal: input.excludeInternal,
57
+ };
58
+ if (input.includeTags !== undefined) {
59
+ filter.includeTags = input.includeTags;
60
+ }
61
+ if (input.excludeTags !== undefined) {
62
+ filter.excludeTags = input.excludeTags;
63
+ }
64
+ if (input.pathPrefix !== undefined) {
65
+ filter.pathPrefix = input.pathPrefix;
66
+ }
67
+
68
+ const procedures = PROCEDURE_REGISTRY.getAll();
69
+ const mcpTools = proceduresToMcpTools(procedures, filter);
70
+
71
+ const tools: McpToolInfo[] = mcpTools.map((tool) => ({
72
+ name: tool.name,
73
+ description: tool.description,
74
+ path: tool.procedurePath,
75
+ streaming: tool.streaming,
76
+ tags: tool.tags,
77
+ }));
78
+
79
+ return {
80
+ tools,
81
+ count: tools.length,
82
+ };
83
+ },
84
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * mcp.serve Procedure
3
+ *
4
+ * Start MCP server to expose procedures as tools.
5
+ *
6
+ * Which procedures are tools is a security decision. Before, `mcp.serve` exposed every
7
+ * procedure of the registry, `shell.exec` too (deep dive, architecture review 3.2). Now the tools
8
+ * come from `namespaces`/`exclude`, or from the `mcpNamespaces`/`mcpExclude` of the bundles
9
+ * (default: `@procedures/bundle-mcp`, resolved from the current folder). Without a list,
10
+ * nothing is exposed. The nested calls follow the call policy (`strictCalls`), and each tool call
11
+ * has a time limit.
12
+ */
13
+
14
+ import { z } from "zod";
15
+ import {
16
+ defineProcedure,
17
+ ProcedureServer,
18
+ PROCEDURE_REGISTRY,
19
+ timeoutInterceptor,
20
+ type AnyProcedure,
21
+ type ProcedurePath,
22
+ } from "@procedures/client";
23
+ import { McpServerTransport } from "../../transport/mcp-transport.js";
24
+ import type { McpServerTransportOptions } from "../../types.js";
25
+ import { exposeRule, importBundles, mergeSurfaces } from "../../surface.js";
26
+
27
+ /** The default bundle of the tool surface. */
28
+ export const DEFAULT_SERVE_BUNDLE = "@procedures/bundle-mcp";
29
+
30
+ const McpServeInputSchema = z.object({
31
+ /**
32
+ * Transport type (default: "stdio"). Only stdio is implemented: the SSE transport needs an
33
+ * HTTP server integration, and accepting "sse" here only led to an error (BUGS-2026-07 L28).
34
+ */
35
+ transport: z.enum(["stdio"]).default("stdio").describe("Transport type (default: \"stdio\"). Only stdio is implemented: the SSE transport needs an HTTP server integration, and accepting \"sse\" here only led to an error (BUGS-2026-07 L28)."),
36
+ /** Server name for MCP (default: "procedure-server") */
37
+ name: z.string().optional().describe("Server name for MCP (default: \"procedure-server\")"),
38
+ /** Server version for MCP (default: "1.0.0") */
39
+ version: z.string().optional().describe("Server version for MCP (default: \"1.0.0\")"),
40
+ /** Whether to auto-register all procedures (default: true) */
41
+ autoRegister: z.boolean().default(true).describe("Whether to auto-register all procedures (default: true)"),
42
+ /** Enable debug logging */
43
+ debug: z.boolean().default(false).describe("Enable debug logging"),
44
+ /** The namespaces (first path segments) of the tools. With this list, the bundles give no tools. */
45
+ namespaces: z.array(z.string()).optional().describe("The namespaces (first path segments) of the tools. With this list, the bundles give no tools."),
46
+ /** Procedure keys of the tool namespaces that are not tools. */
47
+ exclude: z.array(z.string()).optional().describe("Procedure keys of the tool namespaces that are not tools."),
48
+ /** The bundles whose mcpNamespaces and mcpExclude give the tools, when `namespaces` is not given. */
49
+ bundles: z.array(z.string()).optional().describe("The bundles whose mcpNamespaces and mcpExclude give the tools, when `namespaces` is not given."),
50
+ /** True (the default): a procedure of code without metadata.calls can call only tools. */
51
+ strictCalls: z.boolean().default(true).describe("True (the default): a procedure of code without metadata.calls can call only tools."),
52
+ /** The time limit of each tool call, in milliseconds (default: 600000). 0: no limit. */
53
+ toolTimeoutMs: z.number().int().min(0).default(600_000).describe("The time limit of each tool call, in milliseconds (default: 600000). 0: no limit."),
54
+ });
55
+
56
+ type McpServeInput = z.infer<typeof McpServeInputSchema>;
57
+
58
+ interface McpServeOutput {
59
+ success: boolean;
60
+ transport: string;
61
+ toolCount: number;
62
+ serverName: string;
63
+ /** Why the server has no tools, when it has none. */
64
+ warning?: string;
65
+ }
66
+
67
+ const McpServeOutputSchema: {
68
+ parse: (data: unknown) => McpServeOutput;
69
+ safeParse: (data: unknown) => { success: true; data: McpServeOutput };
70
+ } = {
71
+ parse: (data: unknown) => data as McpServeOutput,
72
+ safeParse: (data: unknown) => ({ success: true as const, data: data as McpServeOutput }),
73
+ };
74
+
75
+ /** The options of `prepareServe`: the tool surface and the rules of the server. */
76
+ export interface ServeOptions {
77
+ autoRegister?: boolean | undefined;
78
+ namespaces?: readonly string[] | undefined;
79
+ exclude?: readonly string[] | undefined;
80
+ bundles?: readonly string[] | undefined;
81
+ strictCalls?: boolean | undefined;
82
+ toolTimeoutMs?: number | undefined;
83
+ }
84
+
85
+ /** The server of `mcp.serve`, before its transport starts. */
86
+ export interface ServeSetup {
87
+ server: ProcedureServer;
88
+ expose: (path: ProcedurePath) => boolean;
89
+ /** Why the server has no tools, when it has none. */
90
+ warning?: string;
91
+ }
92
+
93
+ /**
94
+ * This function builds the procedure server of `mcp.serve`: its expose rule, its call policy and
95
+ * its interceptors. The bundles resolve from `from` (default: the current folder).
96
+ */
97
+ export async function prepareServe(input: ServeOptions, from: string = process.cwd()): Promise<ServeSetup> {
98
+ let expose: (path: ProcedurePath) => boolean = () => false;
99
+ let warning: string | undefined;
100
+ if (input.namespaces) {
101
+ expose = exposeRule({ namespaces: input.namespaces, exclude: input.exclude });
102
+ } else {
103
+ const bundles = input.bundles ?? [DEFAULT_SERVE_BUNDLE];
104
+ try {
105
+ const surface = mergeSurfaces(await importBundles(bundles, from));
106
+ if (surface) {
107
+ expose = exposeRule({ namespaces: surface.namespaces, exclude: [...(surface.exclude ?? []), ...(input.exclude ?? [])] });
108
+ } else {
109
+ warning = `The bundles ${bundles.join(", ")} give no mcpNamespaces, so no procedure is a tool. Give namespaces.`;
110
+ }
111
+ } catch (error) {
112
+ warning =
113
+ `The bundles ${bundles.join(", ")} cannot be loaded from ${from} (${(error as Error).message}), ` +
114
+ `so no procedure is a tool. Give namespaces, or run in a project that has the bundles.`;
115
+ }
116
+ }
117
+
118
+ const server = new ProcedureServer({
119
+ autoRegister: input.autoRegister ?? true,
120
+ registry: PROCEDURE_REGISTRY,
121
+ expose,
122
+ strictCalls: input.strictCalls ?? true,
123
+ interceptors: [timeoutInterceptor({ ms: input.toolTimeoutMs ?? 600_000 })],
124
+ });
125
+ return warning === undefined ? { server, expose } : { server, expose, warning };
126
+ }
127
+
128
+ export const mcpServeProcedure: AnyProcedure = defineProcedure({
129
+ path: ["mcp", "serve"],
130
+ input: McpServeInputSchema,
131
+ output: McpServeOutputSchema,
132
+ metadata: {
133
+ description: "Start MCP server to expose procedures as tools",
134
+ tags: ["mcp", "server"],
135
+ },
136
+ handler: async (input: McpServeInput): Promise<McpServeOutput> => {
137
+ const serverName = input.name ?? "procedure-server";
138
+ const serverVersion = input.version ?? "1.0.0";
139
+ const { server, expose, warning } = await prepareServe(input);
140
+
141
+ // Build transport options
142
+ const options: McpServerTransportOptions = {
143
+ transport: input.transport,
144
+ serverInfo: {
145
+ name: serverName,
146
+ version: serverVersion,
147
+ },
148
+ debug: input.debug,
149
+ toolFilter: { include: expose },
150
+ };
151
+
152
+ // Create MCP transport
153
+ const mcpTransport = new McpServerTransport(server, options);
154
+
155
+ // Add transport and start
156
+ server.addTransport(mcpTransport);
157
+ await server.start();
158
+
159
+ const state = mcpTransport.getState();
160
+ if (warning) console.error(`[mcp.serve] ${warning}`);
161
+
162
+ return {
163
+ success: true,
164
+ transport: input.transport,
165
+ toolCount: state.toolCount,
166
+ serverName,
167
+ ...(warning ? { warning } : {}),
168
+ };
169
+ },
170
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Importing this file registers the MCP procedures in the global registry (DECISIONS-2026-10,
3
+ * "Every package root registers its procedures"). The procedures as data are in `module.ts`,
4
+ * which registers nothing: a host with its own registry uses that one.
5
+ */
6
+
7
+ import { PROCEDURE_REGISTRY } from "@procedures/client";
8
+ import { mcpModule } from "./module.js";
9
+
10
+ export * from "./module.js";
11
+
12
+ /**
13
+ * Register all MCP procedures.
14
+ */
15
+ export function registerMcpProcedures(): void {
16
+ for (const proc of mcpModule.procedures) {
17
+ if (!PROCEDURE_REGISTRY.has(proc.path)) {
18
+ PROCEDURE_REGISTRY.register(proc);
19
+ }
20
+ }
21
+ }
22
+
23
+ // Auto-register when this module is imported
24
+ registerMcpProcedures();
package/src/surface.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The tool surface of an MCP server: which procedures are tools.
3
+ *
4
+ * A bundle (for example `@procedures/bundle-mcp`) registers procedures when it is imported.
5
+ * Its `register.js` exports `mcpNamespaces` (the first path segments of the tools) and
6
+ * `mcpExclude` (procedure keys of those namespaces that are not tools). The dev-tools server
7
+ * (`impl-mcp-dev`) and `mcp.serve` build their expose rule from these lists.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+ import { surfaceRule, type ProcedurePath, type ToolSurface } from "@procedures/client";
14
+
15
+ /** The tools of a server: the namespaces, less the excluded procedure keys (the core type). */
16
+ export type { ToolSurface };
17
+
18
+ /** The exports of a bundle's `register.js` that describe its tools. */
19
+ export interface BundleSurfaceModule {
20
+ mcpNamespaces?: readonly string[];
21
+ mcpExclude?: readonly string[];
22
+ }
23
+
24
+ /** The expose rule of a tool surface: `surfaceRule()` of the core host policy (roadmap 2.3). */
25
+ export function exposeRule(surface: ToolSurface): (path: ProcedurePath) => boolean {
26
+ return surfaceRule(surface);
27
+ }
28
+
29
+ /**
30
+ * The union of the surfaces of some bundles. Undefined when no bundle has `mcpNamespaces`: such
31
+ * bundles say nothing about their tools.
32
+ */
33
+ export function mergeSurfaces(modules: readonly BundleSurfaceModule[]): ToolSurface | undefined {
34
+ let namespaces: Set<string> | undefined;
35
+ const exclude = new Set<string>();
36
+ for (const module of modules) {
37
+ if (Array.isArray(module.mcpNamespaces)) {
38
+ namespaces ??= new Set();
39
+ for (const namespace of module.mcpNamespaces) namespaces.add(namespace);
40
+ }
41
+ for (const key of module.mcpExclude ?? []) exclude.add(key);
42
+ }
43
+ return namespaces ? { namespaces: [...namespaces].sort(), exclude: [...exclude].sort() } : undefined;
44
+ }
45
+
46
+ /**
47
+ * The file URL of a bundle's `register.js`, found in the `node_modules` folders from `from`
48
+ * upward. The bundle's `exports` map gives the file. (`require.resolve` cannot do it: a bundle
49
+ * exports `register.js` for `import` only.)
50
+ */
51
+ export function resolveBundleRegister(bundle: string, from: string): string {
52
+ let dir = resolve(from);
53
+ for (;;) {
54
+ const folder = join(dir, "node_modules", ...bundle.split("/"));
55
+ const manifestPath = join(folder, "package.json");
56
+ if (existsSync(manifestPath)) {
57
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { exports?: Record<string, unknown> };
58
+ const entry = manifest.exports?.["./register.js"];
59
+ const target =
60
+ typeof entry === "string"
61
+ ? entry
62
+ : ((entry as { import?: string; default?: string } | undefined)?.import ??
63
+ (entry as { default?: string } | undefined)?.default ??
64
+ "./dist/register.js");
65
+ return pathToFileURL(join(folder, target)).href;
66
+ }
67
+ const parent = dirname(dir);
68
+ if (parent === dir) throw new Error(`Cannot find ${bundle} from ${from}`);
69
+ dir = parent;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * This function imports the `register.js` of each bundle, which registers its procedures, and
75
+ * returns the modules. With `from`, a bundle resolves from that folder (a project), not from
76
+ * this package.
77
+ */
78
+ export async function importBundles(bundles: readonly string[], from?: string): Promise<BundleSurfaceModule[]> {
79
+ const modules: BundleSurfaceModule[] = [];
80
+ for (const bundle of bundles) {
81
+ const target = from ? resolveBundleRegister(bundle, from) : `${bundle}/register.js`;
82
+ modules.push((await import(target)) as BundleSurfaceModule);
83
+ }
84
+ return modules;
85
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Transport exports.
3
+ */
4
+
5
+ export { McpServerTransport } from "./mcp-transport.js";
6
+ export { createStdioTransport } from "./stdio.js";
7
+ export { createSseTransport } from "./sse.js";