typespec-http-mcp 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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The arm that answers for a status, in the document's own precedence order.
3
+ *
4
+ * Exact code, then the `NXX` range, then `default`. Sorting the rule out once here is what stops
5
+ * every application re-deriving it, and getting it silently wrong when a `404` and a `4XX` both apply.
6
+ */
7
+ export function armFor(arms, status) {
8
+ return (arms.find((arm) => arm.status === status) ??
9
+ arms.find((arm) => arm.status === `${Math.floor(status / 100)}XX`) ??
10
+ arms.find((arm) => arm.status === "default"));
11
+ }
12
+ /**
13
+ * A response body that does not match the schema the document publishes for its status.
14
+ *
15
+ * **Thrown, so the application decides what a contract failure answers with**, the same class and
16
+ * the same fields as the sibling server emitter's. The MCP server turns a thrown error into a
17
+ * tool-execution error; an application that wants something else catches it around `registerTools`'
18
+ * handlers.
19
+ *
20
+ * `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
21
+ * application that logs them should decide whether its results may contain anything it would not
22
+ * log.
23
+ */
24
+ export class ResponseContractError extends Error {
25
+ operationId;
26
+ status;
27
+ issues;
28
+ constructor(operationId, status, issues) {
29
+ super(`${operationId} answered ${status} with a body its document does not permit`);
30
+ this.operationId = operationId;
31
+ this.status = status;
32
+ this.issues = issues;
33
+ this.name = "ResponseContractError";
34
+ }
35
+ }
36
+ /**
37
+ * A handler answered with a status its operation does not declare.
38
+ *
39
+ * **Unreachable from a typed handler.** The generated `Tools` interface types every result as the
40
+ * union of the declared statuses, so a literal outside it does not compile. This is what a CAST
41
+ * produces, or a result that crossed a boundary the type system cannot see into. Thrown rather than
42
+ * answered, because answering would publish a status the contract does not state.
43
+ */
44
+ export class UndeclaredStatusError extends Error {
45
+ operationId;
46
+ result;
47
+ constructor(operationId, result) {
48
+ const status = typeof result === "object" && result !== null && "status" in result
49
+ ? String(result.status)
50
+ : "no status";
51
+ super(`${operationId} answered ${status}, which its document does not declare`);
52
+ this.operationId = operationId;
53
+ this.result = result;
54
+ this.name = "UndeclaredStatusError";
55
+ }
56
+ }
57
+ /** A media type whose body is a JSON value. The rule the library and the server emitter apply. */
58
+ function isJsonMediaType(type) {
59
+ return /^application\/(json|.*\+json)$/.test(type);
60
+ }
61
+ /** RFC 9110 `token`, less `*`, which names a range rather than a type. */
62
+ const MEDIA_TOKEN = "[!#$%&'+.^_`|~0-9A-Za-z-]+";
63
+ const SERVED_MEDIA_TYPE = new RegExp(`^(${MEDIA_TOKEN})/${MEDIA_TOKEN}\\s*(;.*)?$`);
64
+ /**
65
+ * Whether an offered media type, which may be a range such as `image/*`, admits a served one.
66
+ *
67
+ * The sibling server emitter's `mediaTypeWithin`, rule for rule: a range admits a concrete type inside
68
+ * it, never its own spelling or a type with no subtype; type names compare case-insensitively
69
+ * (RFC 9110 section 8.3.1), and parameters such as `charset` are allowed.
70
+ */
71
+ function admits(offered, served) {
72
+ if (!offered.endsWith("/*"))
73
+ return offered === served;
74
+ const type = SERVED_MEDIA_TYPE.exec(served)?.[1];
75
+ if (type === undefined)
76
+ return false;
77
+ if (offered === "*/*")
78
+ return true;
79
+ return type.toLowerCase() === offered.slice(0, -2).toLowerCase();
80
+ }
81
+ /**
82
+ * The media type a result is served as, or `undefined` where the arm names none.
83
+ *
84
+ * Where the arm offers several, or only a wildcard range, the handler names the one it answers with,
85
+ * and a type the arm does not offer is as undeclared as a status it does not. A range admits every
86
+ * type inside it: `image/*` is what lets a handler answer `image/png`.
87
+ */
88
+ function mediaTypeOf(arm, contentType, operationId, result) {
89
+ const offered = arm.contentTypes ?? [];
90
+ const choosing = offered.length > 1 || offered.some((type) => type.endsWith("/*"));
91
+ if (!choosing)
92
+ return offered[0];
93
+ if (typeof contentType === "string" && offered.some((type) => admits(type, contentType))) {
94
+ return contentType;
95
+ }
96
+ throw new UndeclaredStatusError(operationId, result);
97
+ }
98
+ /** A body that is not what its arm says it is, as one issue in Zod's own shape. */
99
+ function refusedBody(input, message) {
100
+ return [{ code: "custom", path: [], message, input }];
101
+ }
102
+ /**
103
+ * The bytes of a binary body, which a handler may hand over in any of the forms the result type admits.
104
+ *
105
+ * A stream is buffered: a tool result is one message, and the block carries the whole of it.
106
+ * `Response` reads it, because both runtimes this output runs on, Node and workerd, provide it.
107
+ */
108
+ async function bytesOf(body) {
109
+ if (body instanceof Uint8Array)
110
+ return body;
111
+ if (body instanceof ArrayBuffer)
112
+ return new Uint8Array(body);
113
+ if (body instanceof ReadableStream)
114
+ return new Uint8Array(await new Response(body).arrayBuffer());
115
+ return undefined;
116
+ }
117
+ /** Base64, in chunks, so a large body does not exceed the argument limit of one call. */
118
+ function base64Of(bytes) {
119
+ let binary = "";
120
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
121
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
122
+ }
123
+ return btoa(binary);
124
+ }
125
+ /**
126
+ * The MCP result for what a handler returned: `{ status, body, headers }` for a response the
127
+ * document declares.
128
+ *
129
+ * **In the runtime rather than in `deps`, on the same test the sibling emitter applies**: every rule
130
+ * below is published -- by the document (the arms, their bodies, their headers) or by the MCP
131
+ * specification (the result shape, its content blocks, `isError`, the text mirror). An application
132
+ * forced to supply this would be re-implementing two standards, and getting either wrong differently
133
+ * from everybody else.
134
+ *
135
+ * **What is answered is the PARSED body, not the one the handler returned**, as on the HTTP surface:
136
+ * a schema that strips undeclared keys strips them here too, so a field the document does not publish
137
+ * does not reach a model.
138
+ *
139
+ * Asynchronous because a binary body may be a stream, which is read to its end before it is answered.
140
+ */
141
+ export async function toolResult(declared, operationId, result) {
142
+ const answer = typeof result === "object" && result !== null ? result : {};
143
+ const { status } = answer;
144
+ const arm = typeof status === "number" ? armFor(declared.arms, status) : undefined;
145
+ if (arm === undefined || typeof status !== "number") {
146
+ throw new UndeclaredStatusError(operationId, result);
147
+ }
148
+ const mediaType = mediaTypeOf(arm, answer.contentType, operationId, result);
149
+ /**
150
+ * **`isError` is decided from the STATUS, not from whether the arm is an error model.**
151
+ *
152
+ * MCP's `isError` marks a tool-execution error the model can self-correct from, which is exactly
153
+ * what a declared 4xx or 5xx is: the service's own published failure, with a body the model can
154
+ * read. It is decided from the status because `armFor` may resolve to a range or `default`, whose
155
+ * numeric status only the result carries.
156
+ */
157
+ const isError = status >= 400;
158
+ const toolAnswer = { content: [] };
159
+ /** Whether the body was answered as JSON text, which `structuredContent` then mirrors. */
160
+ let servedAsJson = false;
161
+ /**
162
+ * A bodyless arm carries nothing a model can read, and inventing a confirmation sentence would
163
+ * state something the document does not, so `content` stays `[]`: present, an array, and honest.
164
+ */
165
+ if (arm.schema !== undefined && answer.body !== undefined) {
166
+ const kind = declared.bodies?.[String(arm.status)];
167
+ if (mediaType === undefined || isJsonMediaType(mediaType)) {
168
+ /**
169
+ * **Validated wherever the body is served as JSON**, which is also the only form a tool result
170
+ * can carry structurally. An arm naming no media type is a hand-written arm, and a tool
171
+ * result's text is JSON, so it is validated too.
172
+ */
173
+ const parsed = arm.schema.safeParse(answer.body);
174
+ if (!parsed.success) {
175
+ throw new ResponseContractError(operationId, status, parsed.error.issues);
176
+ }
177
+ servedAsJson = true;
178
+ // The text mirror is kept even beside `structuredContent`: MCP recommends it for clients
179
+ // that read only `content`.
180
+ toolAnswer.content = [{ type: "text", text: JSON.stringify(parsed.data) }];
181
+ /**
182
+ * **Where this arm's schema IS the published `outputSchema`.** MCP obliges a tool that
183
+ * declares an `outputSchema` to return conforming `structuredContent`, and a failure's body
184
+ * does not conform to the success schema the tool published.
185
+ */
186
+ if (declared.outputSchema !== undefined && arm.schema === declared.outputSchema) {
187
+ toolAnswer.structuredContent = parsed.data;
188
+ }
189
+ }
190
+ else if (kind === "text") {
191
+ /**
192
+ * **A string body under a media type that is not JSON IS the text**, so it is checked against
193
+ * its schema exactly as the sibling server emitter checks it, and the parsed string answered.
194
+ */
195
+ const parsed = arm.schema.safeParse(answer.body);
196
+ if (!parsed.success) {
197
+ throw new ResponseContractError(operationId, status, parsed.error.issues);
198
+ }
199
+ toolAnswer.content = [{ type: "text", text: String(parsed.data) }];
200
+ }
201
+ else if (kind === "binary") {
202
+ /**
203
+ * **Raw bytes are answered as the block MCP defines for their media type**, carrying the data
204
+ * as base64 and the media type served. The emitter refuses every other binary media type, so
205
+ * one reaching here was declared by hand.
206
+ */
207
+ const block = /^(image|audio)\//.exec(mediaType)?.[1];
208
+ if (block !== "image" && block !== "audio") {
209
+ throw new ResponseContractError(operationId, status, refusedBody(answer.body, `no MCP content block carries a binary body as ${mediaType}`));
210
+ }
211
+ const bytes = await bytesOf(answer.body);
212
+ if (bytes === undefined) {
213
+ throw new ResponseContractError(operationId, status, refusedBody(answer.body, "a binary body is a Uint8Array, an ArrayBuffer or a ReadableStream"));
214
+ }
215
+ toolAnswer.content = [{ type: block, data: base64Of(bytes), mimeType: mediaType }];
216
+ }
217
+ else {
218
+ /**
219
+ * **A model under a media type that is not JSON is text the handler serialised**, which the
220
+ * schema does not describe, so it is answered as it is. `unvalidated-response-media-type`
221
+ * said so at compile time. Anything but a string is not that text.
222
+ */
223
+ if (typeof answer.body !== "string") {
224
+ throw new ResponseContractError(operationId, status, refusedBody(answer.body, `a body answered as ${mediaType} is the text the handler supplies`));
225
+ }
226
+ toolAnswer.content = [{ type: "text", text: answer.body }];
227
+ }
228
+ }
229
+ /**
230
+ * **A success the published `outputSchema` does not describe fails here, by name.**
231
+ *
232
+ * The SDK refuses a result that is not an error and carries no `structuredContent` once a tool
233
+ * declares an `outputSchema`. The emitter publishes one only where every 2xx and 3xx response the
234
+ * document declares shares its body, but `default` covers the statuses nobody listed, 2xx and 3xx
235
+ * among them. Such a success is answered only if its body also meets the published schema;
236
+ * otherwise it is the contract failure it is, rather than the SDK's "no structured content".
237
+ *
238
+ * **The body the HANDLER returned is checked, not the one its own arm parsed.** An object schema
239
+ * strips the keys it does not declare, so a `Problem`-parsed body has already lost every key a
240
+ * `Widget` needs: checking that would refuse a body that meets both. The text mirror is then the
241
+ * published schema's parse, because that is what `structuredContent` carries.
242
+ */
243
+ if (!isError &&
244
+ declared.outputSchema !== undefined &&
245
+ toolAnswer.structuredContent === undefined) {
246
+ const conforming = declared.outputSchema.safeParse(answer.body);
247
+ if (!conforming.success) {
248
+ throw new ResponseContractError(operationId, status, conforming.error.issues);
249
+ }
250
+ toolAnswer.structuredContent = conforming.data;
251
+ if (servedAsJson) {
252
+ toolAnswer.content = [{ type: "text", text: JSON.stringify(conforming.data) }];
253
+ }
254
+ }
255
+ if (isError)
256
+ toolAnswer.isError = true;
257
+ /**
258
+ * **Response headers go to `_meta`, keyed by the WIRE name, never to `structuredContent`.**
259
+ *
260
+ * Putting them in `structuredContent` breaks the declared `outputSchema`, which MCP forbids;
261
+ * dropping them is a loss the consumer cannot see. `_meta` is MCP's slot for implementation data
262
+ * outside the tool contract, and the key is namespaced so it collides with nothing. Only the
263
+ * headers the arm declares are carried, and an optional one the handler left out is omitted. A
264
+ * model cannot read `_meta`, so a value that matters to a caller belongs in the response body --
265
+ * which `headers-only-success` warns about.
266
+ */
267
+ const supplied = typeof answer.headers === "object" && answer.headers !== null
268
+ ? answer.headers
269
+ : {};
270
+ const carried = {};
271
+ for (const header of arm.headers ?? []) {
272
+ const value = supplied[header.name];
273
+ if (value !== undefined)
274
+ carried[header.name] = value;
275
+ }
276
+ if (Object.keys(carried).length > 0) {
277
+ toolAnswer._meta = { "typespec-http-mcp/response-headers": carried };
278
+ }
279
+ return toolAnswer;
280
+ }
@@ -0,0 +1,49 @@
1
+ import { type Program } from "@typespec/compiler";
2
+ import type { HttpOperation, HttpService } from "@typespec/http";
3
+ import type { EmittedRoute } from "typespec-http-zod";
4
+ /**
5
+ * Which operations become tools.
6
+ *
7
+ * **`getAllTags`, never `getTags`, and the difference is a silent defect.** `getTags` returns only an
8
+ * operation's OWN tags, so an operation declared inside a `@tag`-carrying `interface` reports none.
9
+ * Measured on a fixture tagging all three levels:
10
+ *
11
+ * ```
12
+ * GET /things getTags: ["agent"] getAllTags: ["service-level","agent"]
13
+ * DELETE /things/{id} getTags: [] getAllTags: ["service-level"]
14
+ * POST /bulk getTags: [] getAllTags: ["service-level","agent","admin"]
15
+ * ```
16
+ *
17
+ * where `/bulk` is a member of a tagged interface. Tagging a whole interface is the obvious way to
18
+ * mark a group of operations as tools, so `getTags` would break the primary use case invisibly --
19
+ * the same failure shape this estate already recorded for matching operations by NAME, which never
20
+ * succeeded for an operation inside an `interface`.
21
+ *
22
+ * **A `@tag` on the service namespace propagates to every operation.** That is correct and
23
+ * documented: it is how a whole service is marked, and it is also why a tag chosen for grouping and
24
+ * a tag chosen for selection should not be the same word.
25
+ *
26
+ * Selection is by tag because `@typespec/openapi3` PUBLISHES tags. A reader of the document can see
27
+ * exactly which operations are tools, which is what keeps this out of decorator territory.
28
+ */
29
+ export interface Selection {
30
+ /** Operation ids the tag configuration matched. */
31
+ readonly selected: readonly string[];
32
+ /** Every tag any operation in the service carries, for a diagnostic that has to be actionable. */
33
+ readonly available: readonly string[];
34
+ }
35
+ export interface SelectionOptions {
36
+ readonly toolTags: readonly string[] | undefined;
37
+ readonly excludeTags: readonly string[];
38
+ }
39
+ /** The `HttpOperation` behind each emitted route, keyed by verb and path. */
40
+ export declare function operationsById(service: HttpService): ReadonlyMap<string, HttpOperation>;
41
+ /**
42
+ * Look an operation up by verb and path, never by name.
43
+ *
44
+ * **Matching on operation NAME silently fails for every operation declared inside an `interface`**,
45
+ * which is most of them in a real spec -- a defect this estate has already paid for once. The verb
46
+ * and the path are what both the route and the document agree on.
47
+ */
48
+ export declare function httpOperationFor(service: HttpService, route: EmittedRoute): HttpOperation | undefined;
49
+ export declare function selectTools(program: Program, service: HttpService, routes: readonly EmittedRoute[], options: SelectionOptions): Selection;
@@ -0,0 +1,59 @@
1
+ import { getAllTags } from "@typespec/compiler";
2
+ /**
3
+ * The lookup key both sides agree on.
4
+ *
5
+ * **Lower-cased, and that is not cosmetic.** `EmittedRoute.verb` is UPPERCASE (`"GET"`) while
6
+ * `HttpOperation.verb` is lowercase (`"get"`) -- measured, on this package's own fixture. A
7
+ * case-sensitive key misses every operation, and the failure is silent in the worst possible way:
8
+ * the emitter selects nothing, reports `no-tools-selected`, and the message blames the tag
9
+ * configuration. It was caught here only because a fixture asserted that an unnarrowed selection
10
+ * selects SOMETHING.
11
+ */
12
+ function keyOf(verb, path) {
13
+ return `${verb.toLowerCase()} ${path}`;
14
+ }
15
+ /** The `HttpOperation` behind each emitted route, keyed by verb and path. */
16
+ export function operationsById(service) {
17
+ const byVerbAndPath = new Map();
18
+ for (const operation of service.operations) {
19
+ byVerbAndPath.set(keyOf(operation.verb, operation.path), operation);
20
+ }
21
+ return byVerbAndPath;
22
+ }
23
+ /**
24
+ * Look an operation up by verb and path, never by name.
25
+ *
26
+ * **Matching on operation NAME silently fails for every operation declared inside an `interface`**,
27
+ * which is most of them in a real spec -- a defect this estate has already paid for once. The verb
28
+ * and the path are what both the route and the document agree on.
29
+ */
30
+ export function httpOperationFor(service, route) {
31
+ return operationsById(service).get(keyOf(route.verb, route.path));
32
+ }
33
+ export function selectTools(program, service, routes, options) {
34
+ const available = new Set();
35
+ const selected = [];
36
+ for (const route of routes) {
37
+ const operation = httpOperationFor(service, route);
38
+ if (operation === undefined)
39
+ continue;
40
+ const tags = getAllTags(program, operation.operation) ?? [];
41
+ for (const tag of tags)
42
+ available.add(tag);
43
+ if (options.excludeTags.some((tag) => tags.includes(tag)))
44
+ continue;
45
+ /**
46
+ * **Unset means every operation, and the alternative was rejected deliberately.** A magic tag
47
+ * name (`"tool"`, `"mcp"`) is a rule an author cannot see in the document and cannot discover,
48
+ * and it makes a correctly configured emitter produce nothing at all.
49
+ *
50
+ * **Union, not intersection.** OpenAPI tags are a flat set with no AND semantics, so "carries
51
+ * any of these" is the only reading the published document supports.
52
+ */
53
+ if (options.toolTags !== undefined && !options.toolTags.some((tag) => tags.includes(tag))) {
54
+ continue;
55
+ }
56
+ selected.push(route.operationId);
57
+ }
58
+ return { selected, available: [...available].toSorted() };
59
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * **The status codes a handler's result may name, as values, held equal to Hono's own types.**
3
+ *
4
+ * One handler object serves both the HTTP surface `typespec-hono` generates and this tool surface,
5
+ * and `typespec-hono` types a result's `status` with Hono's status-code unions. This package's output
6
+ * must not import Hono, so it writes the same sets as literal unions, and a type the two emitters
7
+ * disagree on is a handler that compiles against one surface and not the other.
8
+ * `test/runtime/status-codes.test.ts` reads Hono's declaration file and compares every set here with
9
+ * it, and the agreement arm compiles the two emitted signatures against each other.
10
+ */
11
+ /** Hono's `InfoStatusCode` to `ServerErrorStatusCode`, by the digit a range key starts with. */
12
+ export declare const STATUS_GROUPS: {
13
+ readonly 1: {
14
+ readonly type: "InfoStatusCode";
15
+ readonly codes: readonly [100, 101, 102, 103];
16
+ };
17
+ readonly 2: {
18
+ readonly type: "SuccessStatusCode";
19
+ readonly codes: readonly [200, 201, 202, 203, 204, 205, 206, 207, 208, 226];
20
+ };
21
+ readonly 3: {
22
+ readonly type: "RedirectStatusCode";
23
+ readonly codes: readonly [300, 301, 302, 303, 304, 305, 306, 307, 308];
24
+ };
25
+ readonly 4: {
26
+ readonly type: "ClientErrorStatusCode";
27
+ readonly codes: readonly [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451];
28
+ };
29
+ readonly 5: {
30
+ readonly type: "ServerErrorStatusCode";
31
+ readonly codes: readonly [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511];
32
+ };
33
+ };
34
+ /** Hono's `ContentlessStatusCode`: a status that cannot carry a body. */
35
+ export declare const CONTENTLESS_STATUS_CODES: readonly [101, 204, 205, 304];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * **The status codes a handler's result may name, as values, held equal to Hono's own types.**
3
+ *
4
+ * One handler object serves both the HTTP surface `typespec-hono` generates and this tool surface,
5
+ * and `typespec-hono` types a result's `status` with Hono's status-code unions. This package's output
6
+ * must not import Hono, so it writes the same sets as literal unions, and a type the two emitters
7
+ * disagree on is a handler that compiles against one surface and not the other.
8
+ * `test/runtime/status-codes.test.ts` reads Hono's declaration file and compares every set here with
9
+ * it, and the agreement arm compiles the two emitted signatures against each other.
10
+ */
11
+ /** Hono's `InfoStatusCode` to `ServerErrorStatusCode`, by the digit a range key starts with. */
12
+ export const STATUS_GROUPS = {
13
+ 1: { type: "InfoStatusCode", codes: [100, 101, 102, 103] },
14
+ 2: { type: "SuccessStatusCode", codes: [200, 201, 202, 203, 204, 205, 206, 207, 208, 226] },
15
+ 3: { type: "RedirectStatusCode", codes: [300, 301, 302, 303, 304, 305, 306, 307, 308] },
16
+ 4: {
17
+ type: "ClientErrorStatusCode",
18
+ codes: [
19
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
20
+ 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
21
+ ],
22
+ },
23
+ 5: {
24
+ type: "ServerErrorStatusCode",
25
+ codes: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511],
26
+ },
27
+ };
28
+ /** Hono's `ContentlessStatusCode`: a status that cannot carry a body. */
29
+ export const CONTENTLESS_STATUS_CODES = [101, 204, 205, 304];
@@ -0,0 +1,37 @@
1
+ import { type EmittedRoute, type RouteSchemaNames } from "typespec-http-zod";
2
+ import type { SecurityRequirement } from "./runtime.js";
3
+ /**
4
+ * Where emitted output imports its runtime from: the copy this package writes beside it.
5
+ *
6
+ * **Always this module.** `runtime-module` used to replace it, and that made an application own a
7
+ * copy of generated logic -- see the `runtime-module-removed` diagnostic.
8
+ *
9
+ * **Deliberately NOT `./runtime.gen.js`, which is what the sibling server emitter uses.** Both
10
+ * emitters run `typespec-http-zod`, and a consumer who wants both surfaces may point them at one
11
+ * output directory. `emitFile` has no duplicate-path detection -- measured, `@typespec/compiler`
12
+ * 1.15.0 `dist/src/core/emitter-utils.js:19-27` -- so two emitters writing one path is a silent
13
+ * last-writer-wins whose winner is decided by the ORDER of `emit:` in `tspconfig.yaml`. Measured
14
+ * end to end: with divergent `seal-object-schemas`, one order produced 19 unsealed objects and the
15
+ * other 11 unsealed plus 8 sealed, exit 0 and no diagnostic either time.
16
+ *
17
+ * Giving this package its own runtime filename means the only file the two can contest is
18
+ * `schemas.gen.ts`, which is byte-identical under identical options.
19
+ */
20
+ export declare const DEFAULT_RUNTIME_MODULE = "./mcp-runtime.gen.js";
21
+ export interface ToolEntry {
22
+ readonly route: EmittedRoute;
23
+ readonly names: RouteSchemaNames;
24
+ readonly toolName: string;
25
+ readonly requirements: readonly SecurityRequirement[];
26
+ readonly description: string | undefined;
27
+ readonly title: string | undefined;
28
+ }
29
+ export interface RenderedTools {
30
+ readonly text: string;
31
+ /** Operation ids that became tools. */
32
+ readonly emitted: readonly string[];
33
+ }
34
+ export declare function renderTools(entries: readonly ToolEntry[], declaredOperationIds: readonly string[], options: {
35
+ readonly sealed: boolean;
36
+ readonly hint: string | undefined;
37
+ }): RenderedTools;