@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 ADDED
@@ -0,0 +1,21 @@
1
+ # @telorun/mcp-server
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5288f6c: Initial release of the `mcp-server` module.
8
+
9
+ Adds five resource kinds for exposing a Model Context Protocol server from Telo
10
+ manifests: `Mcp.StdioServer` (stdio transport, `Telo.Service`), `Mcp.HttpEndpoint`
11
+ (Streamable-HTTP transport, `Telo.Mount` on `Http.Server`), and three passive
12
+ bundle kinds — `Mcp.Tools`, `Mcp.Resources`, `Mcp.Prompts` (`Telo.Type`). v1 ships
13
+ runtime dispatch for `Mcp.Tools`; `Resources` and `Prompts` are schema-only and
14
+ gain runtime in v2.
15
+
16
+ Bundles compose by reference: a transport's `tools:` array can reference multiple
17
+ bundles (entries are merged with cross-bundle duplicate detection at init), and a
18
+ single bundle can be referenced from both stdio and HTTP transports without
19
+ re-declaration. Each tool entry maps the MCP envelope (`request.{name, arguments,
20
+ meta, session}`) to any `Telo.Invocable` handler via CEL `inputs:` / `result:` /
21
+ `catches:` adapters — the handler stays oblivious to MCP.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 DiglyAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact DiglyAI.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # MCP Server
2
+
3
+ Model Context Protocol (MCP) server resource kinds for Telo: stdio + Streamable HTTP transports, composable tool/resource/prompt bundles.
4
+
5
+ The module mirrors the `http-server` shape: **transport kinds**
6
+ (`Mcp.StdioServer`, `Mcp.HttpEndpoint`) own the listener and a session model;
7
+ **bundle kinds** (`Mcp.Tools`, `Mcp.Resources`, `Mcp.Prompts`) are passive
8
+ declarations that compose by reference. A single bundle can be referenced by
9
+ both stdio and HTTP transports without re-declaration.
10
+
11
+ ## Capabilities at a glance
12
+
13
+ | Kind | Capability | Role |
14
+ | ------------------ | -------------- | ----------------------------------------------------------------------- |
15
+ | `Mcp.StdioServer` | `Telo.Service` | Listens on stdin/stdout (one client per process). Owns one SDK Server. |
16
+ | `Mcp.HttpEndpoint` | `Telo.Mount` | Mounts on `Http.Server` at a path. Per-session SDK Server (Streamable). |
17
+ | `Mcp.Tools` | `Telo.Type` | Bundle of tool entries dispatched via `tools/call`. |
18
+ | `Mcp.Resources` | `Telo.Type` | Bundle of resource entries (schema'd in v1, runtime in v2). |
19
+ | `Mcp.Prompts` | `Telo.Type` | Bundle of prompt entries (schema'd in v1, runtime in v2). |
20
+
21
+ ## Minimal stdio example
22
+
23
+ ```yaml
24
+ kind: Telo.Application
25
+ metadata: { name: my-stdio-mcp }
26
+ targets: [Server]
27
+ ---
28
+ kind: Telo.Import
29
+ metadata: { name: Mcp }
30
+ source: ../modules/mcp-server
31
+ ---
32
+ kind: Telo.Import
33
+ metadata: { name: JS }
34
+ source: ../modules/javascript
35
+ ---
36
+ kind: Mcp.StdioServer
37
+ metadata: { name: Server }
38
+ serverInfo: { name: my-stdio-mcp, version: 1.0.0 }
39
+ tools: [WeatherTools]
40
+ ---
41
+ kind: Mcp.Tools
42
+ metadata: { name: WeatherTools }
43
+ entries:
44
+ - name: get_weather
45
+ description: Get current weather for a city.
46
+ argumentsSchema:
47
+ type: object
48
+ properties:
49
+ city: { type: string }
50
+ required: [city]
51
+ handler:
52
+ kind: JS.Script
53
+ name: GetWeatherImpl
54
+ inputs:
55
+ city: "${{ request.arguments.city }}"
56
+ result:
57
+ content:
58
+ - type: text
59
+ text: "${{ result.summary }}"
60
+ ```
61
+
62
+ ## Tool entries
63
+
64
+ Each `Mcp.Tools.entries[]` row declares everything MCP advertises plus the
65
+ glue that bridges between MCP envelopes and your handler's domain types:
66
+
67
+ | Field | Purpose |
68
+ | ----------------- | ------------------------------------------------------------------------------------------ |
69
+ | `name` | Tool identifier sent in `tools/call`. |
70
+ | `description` | Human-readable summary surfaced to MCP clients. |
71
+ | `argumentsSchema` | JSON Schema validated by the MCP SDK on every call. |
72
+ | `handler` | Any `Telo.Invocable` (e.g. `JS.Script`, `Run.Sequence`, `Ai.Text`). |
73
+ | `inputs` | CEL map: MCP request → handler input. Sees `request.{name,arguments,meta,session}`. |
74
+ | `result` | CEL map: handler output → full MCP `CallToolResult`. Sees `result` and `request`. |
75
+ | `catches` | Maps thrown `InvokeError`s into JSON-RPC error responses (distinct from `result.isError`). |
76
+
77
+ `isError` on `result` signals a _soft_ tool failure where the LLM should read
78
+ the failure as content. `catches:` is for actual `throw`s and produces a
79
+ JSON-RPC error.
80
+
81
+ ## Composition
82
+
83
+ - **Multiple bundles into one transport.** `tools: [WeatherTools, DatabaseTools]` merges entries from both bundles. Duplicate names across bundles throw at init.
84
+ - **One bundle into multiple transports.** Reference the same `Mcp.Tools` from both `Mcp.StdioServer` and `Mcp.HttpEndpoint` — registrations are independent per-transport, no shared runtime state.
85
+
86
+ ## Out of scope for v1
87
+
88
+ - `Mcp.Resources` and `Mcp.Prompts` runtime dispatch — schemas land in v1, controllers in v2.
89
+ - Streaming tool content / progress notifications.
90
+ - Streamable HTTP idle-session GC + max-sessions cap (sessions live until `Http.Server` shuts down).
91
+ - Server-initiated `sampling`, `roots`, OAuth.
92
+ - A polymorphic `Telo.Mount` dispatch protocol — `Mcp.HttpEndpoint` duck-types `register(app, prefix)` to satisfy `Http.Server`'s mount loop today.
93
+
94
+ See [docs/](./docs) for per-kind reference.
@@ -0,0 +1,36 @@
1
+ import { type ControllerContext, type ResourceContext } from "@telorun/sdk";
2
+ import type { FastifyInstance } from "fastify";
3
+ import { type ServerInfo } from "./registry.js";
4
+ interface HttpEndpointManifest {
5
+ metadata: {
6
+ name: string;
7
+ };
8
+ serverInfo: ServerInfo;
9
+ tools?: string[];
10
+ resources?: string[];
11
+ prompts?: string[];
12
+ }
13
+ export declare function register(_ctx: ControllerContext): Promise<void>;
14
+ export declare class McpHttpEndpoint {
15
+ private readonly resource;
16
+ private readonly ctx;
17
+ private readonly sessions;
18
+ private toolsBundles;
19
+ constructor(resource: HttpEndpointManifest, ctx: ResourceContext);
20
+ init(): Promise<void>;
21
+ /** Mount contract — duck-typed against Http.Server's mount loop. The
22
+ * signature matches Http.Api.register(); see plan §3 mount contract.
23
+ *
24
+ * Routes are declared with `app.route(...)` directly rather than via
25
+ * `app.register(plugin, { prefix })`. The latter is async (the plugin
26
+ * loads inside `app.ready()`); declaring the route synchronously removes
27
+ * any ordering coupling with the host's `app.listen()` call. Both
28
+ * `<prefix>` and `<prefix>/` are registered so trailing-slash variants
29
+ * both reach the handler. */
30
+ register(app: FastifyInstance, prefix?: string): void;
31
+ private handleRequest;
32
+ private createSession;
33
+ private closeAllSessions;
34
+ }
35
+ export declare function create(resource: HttpEndpointManifest, ctx: ResourceContext): Promise<McpHttpEndpoint>;
36
+ export {};
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4
+ import { RuntimeError } from "@telorun/sdk";
5
+ import { buildServer } from "./registry.js";
6
+ export async function register(_ctx) { }
7
+ export class McpHttpEndpoint {
8
+ resource;
9
+ ctx;
10
+ sessions = new Map();
11
+ toolsBundles = [];
12
+ constructor(resource, ctx) {
13
+ this.resource = resource;
14
+ this.ctx = ctx;
15
+ }
16
+ async init() {
17
+ if ((this.resource.resources ?? []).length > 0 || (this.resource.prompts ?? []).length > 0) {
18
+ throw new RuntimeError("ERR_MCP_V2_NOT_IMPLEMENTED", `Mcp.HttpEndpoint[${this.resource.metadata.name}]: resources/prompts are schema-only in v1; runtime dispatch is v2 work`);
19
+ }
20
+ this.toolsBundles = (this.resource.tools ?? []).map((bundleName) => {
21
+ const inst = this.ctx.moduleContext.getInstance(bundleName);
22
+ if (!inst) {
23
+ throw new RuntimeError("ERR_MCP_BUNDLE_NOT_FOUND", `Mcp.HttpEndpoint[${this.resource.metadata.name}]: tools bundle '${bundleName}' not found in module scope`);
24
+ }
25
+ return inst;
26
+ });
27
+ }
28
+ /** Mount contract — duck-typed against Http.Server's mount loop. The
29
+ * signature matches Http.Api.register(); see plan §3 mount contract.
30
+ *
31
+ * Routes are declared with `app.route(...)` directly rather than via
32
+ * `app.register(plugin, { prefix })`. The latter is async (the plugin
33
+ * loads inside `app.ready()`); declaring the route synchronously removes
34
+ * any ordering coupling with the host's `app.listen()` call. Both
35
+ * `<prefix>` and `<prefix>/` are registered so trailing-slash variants
36
+ * both reach the handler. */
37
+ register(app, prefix = "") {
38
+ const handler = async (request, reply) => {
39
+ await this.handleRequest(request, reply);
40
+ };
41
+ const methods = ["POST", "GET", "DELETE"];
42
+ // Normalize prefix: strip a trailing slash unless the prefix is exactly
43
+ // "/", so a configured `/mcp/` doesn't expand to `/mcp//`.
44
+ const base = prefix && prefix !== "/" && prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
45
+ if (base && base !== "/") {
46
+ app.route({ method: methods, url: base, handler });
47
+ app.route({ method: methods, url: `${base}/`, handler });
48
+ }
49
+ else {
50
+ app.route({ method: methods, url: "/", handler });
51
+ }
52
+ app.addHook("onClose", async () => {
53
+ await this.closeAllSessions();
54
+ });
55
+ }
56
+ async handleRequest(request, reply) {
57
+ const sessionHeader = (request.headers["mcp-session-id"] ?? "");
58
+ const body = request.body;
59
+ let record;
60
+ if (sessionHeader) {
61
+ record = this.sessions.get(sessionHeader);
62
+ if (!record) {
63
+ reply.code(404);
64
+ reply.header("Content-Type", "application/json");
65
+ reply.send({
66
+ jsonrpc: "2.0",
67
+ error: { code: -32001, message: "Mcp: unknown session" },
68
+ id: null,
69
+ });
70
+ return;
71
+ }
72
+ }
73
+ else if (request.method === "POST" && body && isInitializeRequest(body)) {
74
+ record = await this.createSession();
75
+ }
76
+ else {
77
+ reply.code(400);
78
+ reply.header("Content-Type", "application/json");
79
+ reply.send({
80
+ jsonrpc: "2.0",
81
+ error: {
82
+ code: -32000,
83
+ message: "Mcp: missing Mcp-Session-Id header (or initialize request body)",
84
+ },
85
+ id: null,
86
+ });
87
+ return;
88
+ }
89
+ // Hand the raw request/response off to the SDK transport. Fastify has
90
+ // already parsed the body, so we pass it explicitly — the transport will
91
+ // not re-read the stream.
92
+ reply.hijack();
93
+ await record.transport.handleRequest(request.raw, reply.raw, body);
94
+ }
95
+ async createSession() {
96
+ const sessionContext = { id: "", clientInfo: {}, capabilities: {} };
97
+ // Pre-allocate the SessionRecord shell so the onsessioninitialized
98
+ // closure can capture a stable object reference and register the session
99
+ // synchronously, before the transport writes the initialize response.
100
+ // Registering after `await transport.handleRequest()` would open a race
101
+ // where the client's follow-up request races with the registration.
102
+ const record = { context: sessionContext };
103
+ const transport = new StreamableHTTPServerTransport({
104
+ sessionIdGenerator: () => randomUUID(),
105
+ onsessioninitialized: (id) => {
106
+ sessionContext.id = id;
107
+ this.sessions.set(id, record);
108
+ },
109
+ });
110
+ record.transport = transport;
111
+ record.server = buildServer({
112
+ serverInfo: this.resource.serverInfo,
113
+ toolsBundles: this.toolsBundles,
114
+ sessionResolver: () => sessionContext,
115
+ ctx: this.ctx,
116
+ moduleContext: this.ctx.moduleContext,
117
+ });
118
+ transport.onclose = () => {
119
+ if (sessionContext.id) {
120
+ this.sessions.delete(sessionContext.id);
121
+ }
122
+ };
123
+ await record.server.connect(transport);
124
+ return record;
125
+ }
126
+ async closeAllSessions() {
127
+ const records = Array.from(this.sessions.values());
128
+ this.sessions.clear();
129
+ for (const record of records) {
130
+ const sessionId = record.context.id || "<unbound>";
131
+ try {
132
+ await record.transport.close();
133
+ }
134
+ catch (err) {
135
+ await this.ctx.emitEvent(`${this.resource.metadata.name}.SessionCloseFailed`, {
136
+ sessionId,
137
+ stage: "transport",
138
+ error: errorPayload(err),
139
+ });
140
+ }
141
+ try {
142
+ await record.server.close();
143
+ }
144
+ catch (err) {
145
+ await this.ctx.emitEvent(`${this.resource.metadata.name}.SessionCloseFailed`, {
146
+ sessionId,
147
+ stage: "server",
148
+ error: errorPayload(err),
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ function errorPayload(err) {
155
+ if (err instanceof Error) {
156
+ return { message: err.message, stack: err.stack, code: err.code };
157
+ }
158
+ return { message: String(err) };
159
+ }
160
+ export async function create(resource, ctx) {
161
+ return new McpHttpEndpoint(resource, ctx);
162
+ }
@@ -0,0 +1,45 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ /** Single catches: entry on a tool/resource/prompt entry. `when` is typed
3
+ * loosely because the manifest schema says `type: boolean` but the value at
4
+ * this point is a `CompiledValue` (when the user wrote `${{ ... }}`) or a
5
+ * bare boolean literal (when the user wrote `when: true`/`when: false`).
6
+ * Truthiness checks would mis-classify the literal `false` case as "no
7
+ * when field" — see matchCatch below. */
8
+ export interface CatchEntry {
9
+ code?: string;
10
+ when?: unknown;
11
+ error: {
12
+ code: number;
13
+ message: string;
14
+ data?: unknown;
15
+ };
16
+ }
17
+ /** Resolved entry handed to the registry — handler ref is captured before
18
+ * Phase 5 injection (kind/name) and the live instance is read after. */
19
+ export interface ResolvedToolEntry {
20
+ name: string;
21
+ description?: string;
22
+ argumentsSchema: Record<string, unknown>;
23
+ inputs: Record<string, unknown>;
24
+ result: Record<string, unknown>;
25
+ catches?: CatchEntry[];
26
+ handlerKind: string;
27
+ handlerName: string;
28
+ handler: ResourceInstance;
29
+ }
30
+ /** Module-context-shaped surface used by registry dispatch — matches the
31
+ * shape consumed by http-api-controller's dispatchReturns/dispatchCatches. */
32
+ export interface ModuleLikeContext {
33
+ expandWith: (v: unknown, ctx: Record<string, unknown>) => unknown;
34
+ }
35
+ interface InvokeError {
36
+ code: string;
37
+ message: string;
38
+ data?: unknown;
39
+ }
40
+ /** Pick the first `catches:` entry that matches the thrown InvokeError. An
41
+ * entry matches when *every* declared predicate passes: `code:` (if present)
42
+ * must equal the error's code AND `when:` (if present) must evaluate truthy.
43
+ * An entry with neither field is the catch-all and matches last. */
44
+ export declare function matchCatch(catches: CatchEntry[] | undefined, err: InvokeError, celCtx: Record<string, unknown>, moduleContext: ModuleLikeContext): CatchEntry | undefined;
45
+ export {};
@@ -0,0 +1,22 @@
1
+ /** Pick the first `catches:` entry that matches the thrown InvokeError. An
2
+ * entry matches when *every* declared predicate passes: `code:` (if present)
3
+ * must equal the error's code AND `when:` (if present) must evaluate truthy.
4
+ * An entry with neither field is the catch-all and matches last. */
5
+ export function matchCatch(catches, err, celCtx, moduleContext) {
6
+ if (!catches || catches.length === 0)
7
+ return undefined;
8
+ let fallback;
9
+ for (const entry of catches) {
10
+ if (entry.when === undefined && entry.code === undefined) {
11
+ fallback ??= entry;
12
+ continue;
13
+ }
14
+ if (entry.code !== undefined && entry.code !== err.code)
15
+ continue;
16
+ if (entry.when !== undefined && moduleContext.expandWith(entry.when, celCtx) !== true) {
17
+ continue;
18
+ }
19
+ return entry;
20
+ }
21
+ return fallback;
22
+ }
@@ -0,0 +1,18 @@
1
+ import { type ControllerContext, type ResourceContext } from "@telorun/sdk";
2
+ interface PromptsManifest {
3
+ metadata?: {
4
+ name?: string;
5
+ };
6
+ entries?: unknown[];
7
+ }
8
+ export declare function register(_ctx: ControllerContext): Promise<void>;
9
+ /** v2 runtime — schema-only in v1. See resources-controller.ts for the same
10
+ * pattern. */
11
+ export declare class McpPromptsBundle {
12
+ readonly bundleName: string;
13
+ readonly entries: unknown[];
14
+ constructor(bundleName: string, entries: unknown[]);
15
+ resolveEntries(): never;
16
+ }
17
+ export declare function create(resource: PromptsManifest, _ctx: ResourceContext): Promise<McpPromptsBundle>;
18
+ export {};
@@ -0,0 +1,22 @@
1
+ import { RuntimeError } from "@telorun/sdk";
2
+ export async function register(_ctx) { }
3
+ /** v2 runtime — schema-only in v1. See resources-controller.ts for the same
4
+ * pattern. */
5
+ export class McpPromptsBundle {
6
+ bundleName;
7
+ entries;
8
+ constructor(bundleName, entries) {
9
+ this.bundleName = bundleName;
10
+ this.entries = entries;
11
+ }
12
+ resolveEntries() {
13
+ throw new RuntimeError("ERR_MCP_V2_NOT_IMPLEMENTED", `Mcp.Prompts[${this.bundleName}]: runtime dispatch is v2 work`);
14
+ }
15
+ }
16
+ export async function create(resource, _ctx) {
17
+ const bundleName = resource.metadata?.name;
18
+ if (!bundleName) {
19
+ throw new RuntimeError("ERR_MCP_PROMPTS_INVALID", "Mcp.Prompts: metadata.name is required");
20
+ }
21
+ return new McpPromptsBundle(bundleName, resource.entries ?? []);
22
+ }
@@ -0,0 +1,28 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type ResourceContext } from "@telorun/sdk";
3
+ import { type ModuleLikeContext } from "./outcome.js";
4
+ import type { McpToolsBundle } from "./tools-controller.js";
5
+ export interface ServerInfo {
6
+ name: string;
7
+ version: string;
8
+ }
9
+ export interface BuildOptions {
10
+ serverInfo: ServerInfo;
11
+ toolsBundles: McpToolsBundle[];
12
+ /** Per-session metadata exposed to CEL inputs as `request.session`. */
13
+ sessionResolver: () => SessionContext;
14
+ ctx: ResourceContext;
15
+ moduleContext: ModuleLikeContext;
16
+ }
17
+ export interface SessionContext {
18
+ id: string;
19
+ clientInfo: {
20
+ name?: string;
21
+ version?: string;
22
+ } | Record<string, unknown>;
23
+ capabilities: Record<string, unknown>;
24
+ }
25
+ /** Build a fully-wired SDK Server. For stdio the caller connects this once;
26
+ * for streamable HTTP a fresh Server is built per session, so this is called
27
+ * every time a new Mcp-Session-Id is minted. */
28
+ export declare function buildServer(opts: BuildOptions): Server;
@@ -0,0 +1,84 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { isInvokeError, RuntimeError } from "@telorun/sdk";
4
+ import { matchCatch } from "./outcome.js";
5
+ /** Merge entries from every bundle, throwing if two bundles register the same
6
+ * tool name. The plan calls for the analyzer to also catch this at compile
7
+ * time (§5.1 item 2) — this runtime guard backstops it until that lands. */
8
+ function mergeToolEntries(bundles) {
9
+ const byName = new Map();
10
+ const owners = new Map();
11
+ for (const bundle of bundles) {
12
+ for (const entry of bundle.resolveEntries()) {
13
+ const priorOwner = owners.get(entry.name);
14
+ if (priorOwner) {
15
+ throw new RuntimeError("ERR_MCP_TOOLS_DUPLICATE", `Mcp: duplicate tool name '${entry.name}' across bundles '${priorOwner}' and '${bundle.bundleName}'`);
16
+ }
17
+ owners.set(entry.name, bundle.bundleName);
18
+ byName.set(entry.name, entry);
19
+ }
20
+ }
21
+ return byName;
22
+ }
23
+ /** Build a fully-wired SDK Server. For stdio the caller connects this once;
24
+ * for streamable HTTP a fresh Server is built per session, so this is called
25
+ * every time a new Mcp-Session-Id is minted. */
26
+ export function buildServer(opts) {
27
+ const tools = mergeToolEntries(opts.toolsBundles);
28
+ const server = new Server({ name: opts.serverInfo.name, version: opts.serverInfo.version }, { capabilities: { tools: {} } });
29
+ const advertised = Array.from(tools.values()).map((t) => ({
30
+ name: t.name,
31
+ description: t.description,
32
+ inputSchema: t.argumentsSchema,
33
+ }));
34
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: advertised }));
35
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
36
+ const tool = tools.get(request.params.name);
37
+ if (!tool) {
38
+ throw new RuntimeError("ERR_MCP_UNKNOWN_TOOL", `Mcp: unknown tool '${request.params.name}'`);
39
+ }
40
+ const session = opts.sessionResolver();
41
+ const requestCtx = {
42
+ request: {
43
+ name: tool.name,
44
+ arguments: request.params.arguments ?? {},
45
+ meta: request.params._meta ?? {},
46
+ session: {
47
+ id: session.id,
48
+ clientInfo: session.clientInfo,
49
+ capabilities: session.capabilities,
50
+ },
51
+ },
52
+ };
53
+ const inputs = opts.moduleContext.expandWith(tool.inputs, requestCtx);
54
+ let handlerResult;
55
+ try {
56
+ handlerResult = await opts.ctx.invokeResolved(tool.handlerKind, tool.handlerName, tool.handler, { ...inputs, inputs });
57
+ }
58
+ catch (err) {
59
+ if (!isInvokeError(err))
60
+ throw err;
61
+ const errPayload = { code: err.code, message: err.message, data: err.data };
62
+ const celCtx = { error: errPayload, request: requestCtx.request };
63
+ const matched = matchCatch(tool.catches, errPayload, celCtx, opts.moduleContext);
64
+ if (!matched) {
65
+ throw err;
66
+ }
67
+ const expanded = opts.moduleContext.expandWith(matched.error, celCtx);
68
+ const ipcError = new Error(expanded.message);
69
+ ipcError.code = expanded.code;
70
+ ipcError.data = expanded.data;
71
+ throw ipcError;
72
+ }
73
+ const resultCtx = { result: handlerResult, request: requestCtx.request };
74
+ const rendered = opts.moduleContext.expandWith(tool.result, resultCtx);
75
+ // Schema requires `content` to be present, but the value comes from CEL
76
+ // expansion at runtime so its type can't be enforced statically. Verify
77
+ // the expanded shape here.
78
+ if (!Array.isArray(rendered.content)) {
79
+ throw new RuntimeError("ERR_MCP_RESULT_INVALID", `Mcp: tool '${tool.name}' result.content is not an array of content blocks`);
80
+ }
81
+ return rendered;
82
+ });
83
+ return server;
84
+ }
@@ -0,0 +1,20 @@
1
+ import { type ControllerContext, type ResourceContext } from "@telorun/sdk";
2
+ interface ResourcesManifest {
3
+ metadata?: {
4
+ name?: string;
5
+ };
6
+ entries?: unknown[];
7
+ }
8
+ export declare function register(_ctx: ControllerContext): Promise<void>;
9
+ /** v2 runtime — schema-only in v1. The bundle is created so transport refs
10
+ * resolve cleanly, but transports refuse to wire it up: `Mcp.StdioServer` /
11
+ * `Mcp.HttpEndpoint` throw at init() if `resources:` is non-empty, and
12
+ * `resolveEntries()` throws if anyone calls it directly. */
13
+ export declare class McpResourcesBundle {
14
+ readonly bundleName: string;
15
+ readonly entries: unknown[];
16
+ constructor(bundleName: string, entries: unknown[]);
17
+ resolveEntries(): never;
18
+ }
19
+ export declare function create(resource: ResourcesManifest, _ctx: ResourceContext): Promise<McpResourcesBundle>;
20
+ export {};
@@ -0,0 +1,24 @@
1
+ import { RuntimeError } from "@telorun/sdk";
2
+ export async function register(_ctx) { }
3
+ /** v2 runtime — schema-only in v1. The bundle is created so transport refs
4
+ * resolve cleanly, but transports refuse to wire it up: `Mcp.StdioServer` /
5
+ * `Mcp.HttpEndpoint` throw at init() if `resources:` is non-empty, and
6
+ * `resolveEntries()` throws if anyone calls it directly. */
7
+ export class McpResourcesBundle {
8
+ bundleName;
9
+ entries;
10
+ constructor(bundleName, entries) {
11
+ this.bundleName = bundleName;
12
+ this.entries = entries;
13
+ }
14
+ resolveEntries() {
15
+ throw new RuntimeError("ERR_MCP_V2_NOT_IMPLEMENTED", `Mcp.Resources[${this.bundleName}]: runtime dispatch is v2 work`);
16
+ }
17
+ }
18
+ export async function create(resource, _ctx) {
19
+ const bundleName = resource.metadata?.name;
20
+ if (!bundleName) {
21
+ throw new RuntimeError("ERR_MCP_RESOURCES_INVALID", "Mcp.Resources: metadata.name is required");
22
+ }
23
+ return new McpResourcesBundle(bundleName, resource.entries ?? []);
24
+ }
@@ -0,0 +1,26 @@
1
+ import { type ControllerContext, type ResourceContext } from "@telorun/sdk";
2
+ import { type ServerInfo } from "./registry.js";
3
+ interface StdioServerManifest {
4
+ metadata: {
5
+ name: string;
6
+ };
7
+ serverInfo: ServerInfo;
8
+ tools?: string[];
9
+ resources?: string[];
10
+ prompts?: string[];
11
+ }
12
+ export declare function register(_ctx: ControllerContext): Promise<void>;
13
+ export declare class McpStdioServer {
14
+ private readonly resource;
15
+ private readonly ctx;
16
+ private server;
17
+ private transport;
18
+ private releaseHold;
19
+ private session;
20
+ constructor(resource: StdioServerManifest, ctx: ResourceContext);
21
+ init(): Promise<void>;
22
+ run(): Promise<void>;
23
+ teardown(): Promise<void>;
24
+ }
25
+ export declare function create(resource: StdioServerManifest, ctx: ResourceContext): Promise<McpStdioServer>;
26
+ export {};