@telorun/mcp-server 0.2.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/CHANGELOG.md +21 -0
- package/LICENSE +17 -0
- package/README.md +94 -0
- package/dist/http-endpoint-controller.d.ts +36 -0
- package/dist/http-endpoint-controller.js +162 -0
- package/dist/outcome.d.ts +45 -0
- package/dist/outcome.js +22 -0
- package/dist/prompts-controller.d.ts +18 -0
- package/dist/prompts-controller.js +22 -0
- package/dist/registry.d.ts +28 -0
- package/dist/registry.js +84 -0
- package/dist/resources-controller.d.ts +20 -0
- package/dist/resources-controller.js +24 -0
- package/dist/stdio-server-controller.d.ts +26 -0
- package/dist/stdio-server-controller.js +89 -0
- package/dist/tools-controller.d.ts +43 -0
- package/dist/tools-controller.js +99 -0
- package/package.json +66 -0
- package/src/http-endpoint-controller.ts +204 -0
- package/src/outcome.ts +69 -0
- package/src/prompts-controller.ts +38 -0
- package/src/registry.ts +146 -0
- package/src/resources-controller.ts +40 -0
- package/src/stdio-server-controller.ts +125 -0
- package/src/tools-controller.ts +151 -0
- package/tsconfig.json +14 -0
- package/tsconfig.lib.json +9 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { isInvokeError, type ResourceContext, RuntimeError } from "@telorun/sdk";
|
|
7
|
+
|
|
8
|
+
import { matchCatch, type ModuleLikeContext, type ResolvedToolEntry } from "./outcome.js";
|
|
9
|
+
import type { McpToolsBundle } from "./tools-controller.js";
|
|
10
|
+
|
|
11
|
+
export interface ServerInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BuildOptions {
|
|
17
|
+
serverInfo: ServerInfo;
|
|
18
|
+
toolsBundles: McpToolsBundle[];
|
|
19
|
+
/** Per-session metadata exposed to CEL inputs as `request.session`. */
|
|
20
|
+
sessionResolver: () => SessionContext;
|
|
21
|
+
ctx: ResourceContext;
|
|
22
|
+
moduleContext: ModuleLikeContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionContext {
|
|
26
|
+
id: string;
|
|
27
|
+
clientInfo: { name?: string; version?: string } | Record<string, unknown>;
|
|
28
|
+
capabilities: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Merge entries from every bundle, throwing if two bundles register the same
|
|
32
|
+
* tool name. The plan calls for the analyzer to also catch this at compile
|
|
33
|
+
* time (§5.1 item 2) — this runtime guard backstops it until that lands. */
|
|
34
|
+
function mergeToolEntries(bundles: McpToolsBundle[]): Map<string, ResolvedToolEntry> {
|
|
35
|
+
const byName = new Map<string, ResolvedToolEntry>();
|
|
36
|
+
const owners = new Map<string, string>();
|
|
37
|
+
for (const bundle of bundles) {
|
|
38
|
+
for (const entry of bundle.resolveEntries()) {
|
|
39
|
+
const priorOwner = owners.get(entry.name);
|
|
40
|
+
if (priorOwner) {
|
|
41
|
+
throw new RuntimeError(
|
|
42
|
+
"ERR_MCP_TOOLS_DUPLICATE",
|
|
43
|
+
`Mcp: duplicate tool name '${entry.name}' across bundles '${priorOwner}' and '${bundle.bundleName}'`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
owners.set(entry.name, bundle.bundleName);
|
|
47
|
+
byName.set(entry.name, entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return byName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build a fully-wired SDK Server. For stdio the caller connects this once;
|
|
54
|
+
* for streamable HTTP a fresh Server is built per session, so this is called
|
|
55
|
+
* every time a new Mcp-Session-Id is minted. */
|
|
56
|
+
export function buildServer(opts: BuildOptions): Server {
|
|
57
|
+
const tools = mergeToolEntries(opts.toolsBundles);
|
|
58
|
+
|
|
59
|
+
const server = new Server(
|
|
60
|
+
{ name: opts.serverInfo.name, version: opts.serverInfo.version },
|
|
61
|
+
{ capabilities: { tools: {} } },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const advertised = Array.from(tools.values()).map((t) => ({
|
|
65
|
+
name: t.name,
|
|
66
|
+
description: t.description,
|
|
67
|
+
inputSchema: t.argumentsSchema as Record<string, unknown>,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: advertised }));
|
|
71
|
+
|
|
72
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
73
|
+
const tool = tools.get(request.params.name);
|
|
74
|
+
if (!tool) {
|
|
75
|
+
throw new RuntimeError(
|
|
76
|
+
"ERR_MCP_UNKNOWN_TOOL",
|
|
77
|
+
`Mcp: unknown tool '${request.params.name}'`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const session = opts.sessionResolver();
|
|
82
|
+
const requestCtx: Record<string, unknown> = {
|
|
83
|
+
request: {
|
|
84
|
+
name: tool.name,
|
|
85
|
+
arguments: request.params.arguments ?? {},
|
|
86
|
+
meta: (request.params as { _meta?: unknown })._meta ?? {},
|
|
87
|
+
session: {
|
|
88
|
+
id: session.id,
|
|
89
|
+
clientInfo: session.clientInfo,
|
|
90
|
+
capabilities: session.capabilities,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const inputs = opts.moduleContext.expandWith(tool.inputs, requestCtx) as Record<
|
|
96
|
+
string,
|
|
97
|
+
unknown
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
let handlerResult: unknown;
|
|
101
|
+
try {
|
|
102
|
+
handlerResult = await opts.ctx.invokeResolved(
|
|
103
|
+
tool.handlerKind,
|
|
104
|
+
tool.handlerName,
|
|
105
|
+
tool.handler,
|
|
106
|
+
{ ...inputs, inputs },
|
|
107
|
+
);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (!isInvokeError(err)) throw err;
|
|
110
|
+
const errPayload = { code: err.code, message: err.message, data: err.data };
|
|
111
|
+
const celCtx = { error: errPayload, request: requestCtx.request };
|
|
112
|
+
const matched = matchCatch(tool.catches, errPayload, celCtx, opts.moduleContext);
|
|
113
|
+
if (!matched) {
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
const expanded = opts.moduleContext.expandWith(matched.error, celCtx) as {
|
|
117
|
+
code: number;
|
|
118
|
+
message: string;
|
|
119
|
+
data?: unknown;
|
|
120
|
+
};
|
|
121
|
+
const ipcError: Error & { code?: number; data?: unknown } = new Error(expanded.message);
|
|
122
|
+
ipcError.code = expanded.code;
|
|
123
|
+
ipcError.data = expanded.data;
|
|
124
|
+
throw ipcError;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resultCtx = { result: handlerResult, request: requestCtx.request };
|
|
128
|
+
const rendered = opts.moduleContext.expandWith(tool.result, resultCtx) as Record<
|
|
129
|
+
string,
|
|
130
|
+
unknown
|
|
131
|
+
>;
|
|
132
|
+
|
|
133
|
+
// Schema requires `content` to be present, but the value comes from CEL
|
|
134
|
+
// expansion at runtime so its type can't be enforced statically. Verify
|
|
135
|
+
// the expanded shape here.
|
|
136
|
+
if (!Array.isArray((rendered as { content?: unknown }).content)) {
|
|
137
|
+
throw new RuntimeError(
|
|
138
|
+
"ERR_MCP_RESULT_INVALID",
|
|
139
|
+
`Mcp: tool '${tool.name}' result.content is not an array of content blocks`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return rendered as { content: unknown[] };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return server;
|
|
146
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ControllerContext, type ResourceContext, RuntimeError } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
interface ResourcesManifest {
|
|
4
|
+
metadata?: { name?: string };
|
|
5
|
+
entries?: unknown[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function register(_ctx: ControllerContext): Promise<void> {}
|
|
9
|
+
|
|
10
|
+
/** v2 runtime — schema-only in v1. The bundle is created so transport refs
|
|
11
|
+
* resolve cleanly, but transports refuse to wire it up: `Mcp.StdioServer` /
|
|
12
|
+
* `Mcp.HttpEndpoint` throw at init() if `resources:` is non-empty, and
|
|
13
|
+
* `resolveEntries()` throws if anyone calls it directly. */
|
|
14
|
+
export class McpResourcesBundle {
|
|
15
|
+
constructor(
|
|
16
|
+
public readonly bundleName: string,
|
|
17
|
+
public readonly entries: unknown[],
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
resolveEntries(): never {
|
|
21
|
+
throw new RuntimeError(
|
|
22
|
+
"ERR_MCP_V2_NOT_IMPLEMENTED",
|
|
23
|
+
`Mcp.Resources[${this.bundleName}]: runtime dispatch is v2 work`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function create(
|
|
29
|
+
resource: ResourcesManifest,
|
|
30
|
+
_ctx: ResourceContext,
|
|
31
|
+
): Promise<McpResourcesBundle> {
|
|
32
|
+
const bundleName = resource.metadata?.name;
|
|
33
|
+
if (!bundleName) {
|
|
34
|
+
throw new RuntimeError(
|
|
35
|
+
"ERR_MCP_RESOURCES_INVALID",
|
|
36
|
+
"Mcp.Resources: metadata.name is required",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return new McpResourcesBundle(bundleName, resource.entries ?? []);
|
|
40
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Readable, Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { type ControllerContext, type ResourceContext, RuntimeError } from "@telorun/sdk";
|
|
7
|
+
|
|
8
|
+
import { buildServer, type SessionContext, type ServerInfo } from "./registry.js";
|
|
9
|
+
import type { McpToolsBundle } from "./tools-controller.js";
|
|
10
|
+
|
|
11
|
+
interface StdioServerManifest {
|
|
12
|
+
metadata: { name: string };
|
|
13
|
+
serverInfo: ServerInfo;
|
|
14
|
+
tools?: string[];
|
|
15
|
+
resources?: string[];
|
|
16
|
+
prompts?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function register(_ctx: ControllerContext): Promise<void> {}
|
|
20
|
+
|
|
21
|
+
export class McpStdioServer {
|
|
22
|
+
private server: Server | null = null;
|
|
23
|
+
private transport: StdioServerTransport | null = null;
|
|
24
|
+
private releaseHold: (() => void) | null = null;
|
|
25
|
+
private session: SessionContext;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly resource: StdioServerManifest,
|
|
29
|
+
private readonly ctx: ResourceContext,
|
|
30
|
+
) {
|
|
31
|
+
// stdio has no transport-level session id; mint a stable synthetic UUID at
|
|
32
|
+
// construction so request.session.id is always defined for CEL inputs.
|
|
33
|
+
this.session = { id: randomUUID(), clientInfo: {}, capabilities: {} };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async init() {
|
|
37
|
+
if ((this.resource.resources ?? []).length > 0 || (this.resource.prompts ?? []).length > 0) {
|
|
38
|
+
throw new RuntimeError(
|
|
39
|
+
"ERR_MCP_V2_NOT_IMPLEMENTED",
|
|
40
|
+
`Mcp.StdioServer[${this.resource.metadata.name}]: resources/prompts are schema-only in v1; runtime dispatch is v2 work`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const toolsBundles = (this.resource.tools ?? []).map((bundleName) => {
|
|
45
|
+
const inst = this.ctx.moduleContext.getInstance(bundleName) as McpToolsBundle | undefined;
|
|
46
|
+
if (!inst) {
|
|
47
|
+
throw new RuntimeError(
|
|
48
|
+
"ERR_MCP_BUNDLE_NOT_FOUND",
|
|
49
|
+
`Mcp.StdioServer[${this.resource.metadata.name}]: tools bundle '${bundleName}' not found in module scope`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return inst;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.server = buildServer({
|
|
56
|
+
serverInfo: this.resource.serverInfo,
|
|
57
|
+
toolsBundles,
|
|
58
|
+
sessionResolver: () => this.session,
|
|
59
|
+
ctx: this.ctx,
|
|
60
|
+
moduleContext: this.ctx.moduleContext,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async run(): Promise<void> {
|
|
65
|
+
if (!this.server) {
|
|
66
|
+
throw new Error("Mcp.StdioServer.run() called before init()");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.releaseHold = this.ctx.acquireHold();
|
|
70
|
+
try {
|
|
71
|
+
// ResourceContext types stdin/stdout as the structural NodeJS.ReadableStream
|
|
72
|
+
// / NodeJS.WritableStream interfaces, while the MCP SDK accepts the
|
|
73
|
+
// concrete node:stream `Readable` / `Writable` classes. process.stdin
|
|
74
|
+
// and process.stdout satisfy both shapes; a single cast is enough here.
|
|
75
|
+
this.transport = new StdioServerTransport(
|
|
76
|
+
this.ctx.stdin as unknown as Readable,
|
|
77
|
+
this.ctx.stdout as unknown as Writable,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// The transport's `onclose` fires when stdin reaches EOF (the parent
|
|
81
|
+
// closed the pipe). Releasing the hold then lets the kernel exit.
|
|
82
|
+
this.transport.onclose = () => {
|
|
83
|
+
if (this.releaseHold) {
|
|
84
|
+
this.releaseHold();
|
|
85
|
+
this.releaseHold = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await this.server.connect(this.transport);
|
|
90
|
+
|
|
91
|
+
await this.ctx.emitEvent(`${this.resource.metadata.name}.Listening`, {
|
|
92
|
+
transport: "stdio",
|
|
93
|
+
sessionId: this.session.id,
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (this.releaseHold) {
|
|
97
|
+
this.releaseHold();
|
|
98
|
+
this.releaseHold = null;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async teardown(): Promise<void> {
|
|
105
|
+
if (this.transport) {
|
|
106
|
+
await this.transport.close();
|
|
107
|
+
this.transport = null;
|
|
108
|
+
}
|
|
109
|
+
if (this.server) {
|
|
110
|
+
await this.server.close();
|
|
111
|
+
this.server = null;
|
|
112
|
+
}
|
|
113
|
+
if (this.releaseHold) {
|
|
114
|
+
this.releaseHold();
|
|
115
|
+
this.releaseHold = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function create(
|
|
121
|
+
resource: StdioServerManifest,
|
|
122
|
+
ctx: ResourceContext,
|
|
123
|
+
): Promise<McpStdioServer> {
|
|
124
|
+
return new McpStdioServer(resource, ctx);
|
|
125
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import { RuntimeError } from "@telorun/sdk";
|
|
3
|
+
|
|
4
|
+
import type { CatchEntry, ResolvedToolEntry } from "./outcome.js";
|
|
5
|
+
|
|
6
|
+
/** Manifest shape pre-Phase-5. The handler field is still {kind, name} (or a
|
|
7
|
+
* bare name string) at create() time; Phase 5 swaps it for a live
|
|
8
|
+
* ResourceInstance before consumers (transports) read .entries. */
|
|
9
|
+
interface RawToolEntry {
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
argumentsSchema?: Record<string, unknown>;
|
|
13
|
+
handler?: unknown;
|
|
14
|
+
inputs?: Record<string, unknown>;
|
|
15
|
+
result?: Record<string, unknown>;
|
|
16
|
+
catches?: CatchEntry[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ToolsManifest {
|
|
20
|
+
metadata?: { name?: string; module?: string };
|
|
21
|
+
entries?: RawToolEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CapturedRef {
|
|
25
|
+
kind: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function register(_ctx: ControllerContext): Promise<void> {}
|
|
30
|
+
|
|
31
|
+
/** Passive bundle exposed via ctx.moduleContext.getInstance(name). The
|
|
32
|
+
* transport (StdioServer / HttpEndpoint) reads `.entries` after Phase 5
|
|
33
|
+
* injection — handler refs in each entry are then live ResourceInstances. */
|
|
34
|
+
export class McpToolsBundle {
|
|
35
|
+
constructor(
|
|
36
|
+
public readonly bundleName: string,
|
|
37
|
+
private readonly raw: RawToolEntry[],
|
|
38
|
+
private readonly captured: Map<RawToolEntry, CapturedRef>,
|
|
39
|
+
private readonly ctx: ResourceContext,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
/** Resolve the raw entries into ResolvedToolEntry records suitable for
|
|
43
|
+
* registry consumption. Called from a transport's init() — at that point
|
|
44
|
+
* Phase 5 has injected live handler instances over the captured object refs.
|
|
45
|
+
* String-form refs are resolved here via moduleContext.getInstance(). */
|
|
46
|
+
resolveEntries(): ResolvedToolEntry[] {
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
const resolved: ResolvedToolEntry[] = [];
|
|
49
|
+
for (const raw of this.raw) {
|
|
50
|
+
const name = raw.name;
|
|
51
|
+
if (!name) {
|
|
52
|
+
throw new RuntimeError(
|
|
53
|
+
"ERR_MCP_TOOLS_INVALID",
|
|
54
|
+
`Mcp.Tools[${this.bundleName}]: entry is missing 'name'`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (seen.has(name)) {
|
|
58
|
+
throw new RuntimeError(
|
|
59
|
+
"ERR_MCP_TOOLS_DUPLICATE",
|
|
60
|
+
`Mcp.Tools[${this.bundleName}]: duplicate tool name '${name}' within the same bundle`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
seen.add(name);
|
|
64
|
+
|
|
65
|
+
const ref = this.captured.get(raw);
|
|
66
|
+
if (!ref) {
|
|
67
|
+
throw new RuntimeError(
|
|
68
|
+
"ERR_MCP_TOOLS_NO_HANDLER",
|
|
69
|
+
`Mcp.Tools[${this.bundleName}]: tool '${name}' has no handler reference`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let handlerInstance: ResourceInstance | undefined;
|
|
74
|
+
if (typeof raw.handler === "string") {
|
|
75
|
+
// moduleContext.getInstance(name) throws a plain Error when the
|
|
76
|
+
// resource is missing. Wrap it so misconfigured string refs surface
|
|
77
|
+
// as the MCP-specific ERR_MCP_TOOLS_HANDLER_UNRESOLVED with the
|
|
78
|
+
// bundle/tool location included.
|
|
79
|
+
try {
|
|
80
|
+
handlerInstance = this.ctx.moduleContext.getInstance(ref.name) as ResourceInstance;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new RuntimeError(
|
|
83
|
+
"ERR_MCP_TOOLS_HANDLER_UNRESOLVED",
|
|
84
|
+
`Mcp.Tools[${this.bundleName}]: tool '${name}' handler '${ref.name}' not found: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} else if (raw.handler && typeof raw.handler === "object") {
|
|
88
|
+
// Phase 5 injection has replaced the {kind, name} ref with the live
|
|
89
|
+
// ResourceInstance — unless the referenced resource was missing, in
|
|
90
|
+
// which case Phase 5 leaves the {kind, name} object in place. Detect
|
|
91
|
+
// that case via the absence of `invoke` below.
|
|
92
|
+
handlerInstance = raw.handler as ResourceInstance;
|
|
93
|
+
}
|
|
94
|
+
if (!handlerInstance || typeof (handlerInstance as { invoke?: unknown }).invoke !== "function") {
|
|
95
|
+
throw new RuntimeError(
|
|
96
|
+
"ERR_MCP_TOOLS_HANDLER_UNRESOLVED",
|
|
97
|
+
`Mcp.Tools[${this.bundleName}]: tool '${name}' handler '${ref.kind || "?"}.${ref.name}' did not resolve to a live Invocable — Phase 5 injection may have failed`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resolved.push({
|
|
102
|
+
name,
|
|
103
|
+
description: raw.description,
|
|
104
|
+
argumentsSchema: (raw.argumentsSchema ?? {}) as Record<string, unknown>,
|
|
105
|
+
inputs: (raw.inputs ?? {}) as Record<string, unknown>,
|
|
106
|
+
result: (raw.result ?? {}) as Record<string, unknown>,
|
|
107
|
+
catches: raw.catches,
|
|
108
|
+
handlerKind: ref.kind,
|
|
109
|
+
handlerName: ref.name,
|
|
110
|
+
handler: handlerInstance,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function create(
|
|
118
|
+
resource: ToolsManifest,
|
|
119
|
+
ctx: ResourceContext,
|
|
120
|
+
): Promise<McpToolsBundle> {
|
|
121
|
+
const bundleName = resource.metadata?.name;
|
|
122
|
+
if (!bundleName) {
|
|
123
|
+
throw new RuntimeError(
|
|
124
|
+
"ERR_MCP_TOOLS_INVALID",
|
|
125
|
+
"Mcp.Tools: metadata.name is required",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const entries = resource.entries ?? [];
|
|
129
|
+
|
|
130
|
+
const captured = new Map<RawToolEntry, CapturedRef>();
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const handler = entry.handler;
|
|
133
|
+
if (!handler) {
|
|
134
|
+
throw new RuntimeError(
|
|
135
|
+
"ERR_MCP_TOOLS_INVALID",
|
|
136
|
+
`Mcp.Tools[${bundleName}]: tool '${entry.name ?? "<unnamed>"}' missing handler`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (typeof handler === "object") {
|
|
140
|
+
captured.set(entry, ctx.resolveChildren(handler));
|
|
141
|
+
} else if (typeof handler === "string") {
|
|
142
|
+
// Bare name reference (oneOf: string). Phase 5 injection only replaces
|
|
143
|
+
// {kind, name} object refs — string refs survive as the resource name.
|
|
144
|
+
// We capture the name here so the dispatcher can still look up the
|
|
145
|
+
// instance via moduleContext.getInstance() at invoke time.
|
|
146
|
+
captured.set(entry, { kind: "", name: handler });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return new McpToolsBundle(bundleName, entries, captured, ctx);
|
|
151
|
+
}
|
package/tsconfig.json
ADDED