typespec-http-mcp 0.1.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { type Program } from "@typespec/compiler";
2
- import type { HttpOperation, HttpService } from "@typespec/http";
2
+ import type { HttpOperation } from "@typespec/http";
3
3
  import type { EmittedRoute } from "typespec-http-zod";
4
4
  /**
5
5
  * Which operations become tools.
@@ -36,14 +36,9 @@ export interface SelectionOptions {
36
36
  readonly toolTags: readonly string[] | undefined;
37
37
  readonly excludeTags: readonly string[];
38
38
  }
39
- /** The `HttpOperation` behind each emitted route, keyed by verb and path. */
40
- export declare function operationsById(service: HttpService): ReadonlyMap<string, HttpOperation>;
41
39
  /**
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.
40
+ * **Each route's operation is read by its operation id**, from the library's `operations`. It was
41
+ * looked up by verb and path, which `@sharedRoute` does not make unique, so every member of a shared
42
+ * route was selected by one member's tags.
47
43
  */
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;
44
+ export declare function selectTools(program: Program, routes: readonly Pick<EmittedRoute, "operationId">[], operations: ReadonlyMap<string, HttpOperation>, options: SelectionOptions): Selection;
@@ -1,40 +1,14 @@
1
1
  import { getAllTags } from "@typespec/compiler";
2
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.
3
+ * **Each route's operation is read by its operation id**, from the library's `operations`. It was
4
+ * looked up by verb and path, which `@sharedRoute` does not make unique, so every member of a shared
5
+ * route was selected by one member's tags.
11
6
  */
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) {
7
+ export function selectTools(program, routes, operations, options) {
34
8
  const available = new Set();
35
9
  const selected = [];
36
10
  for (const route of routes) {
37
- const operation = httpOperationFor(service, route);
11
+ const operation = operations.get(route.operationId);
38
12
  if (operation === undefined)
39
13
  continue;
40
14
  const tags = getAllTags(program, operation.operation) ?? [];
package/dist/src/tools.js CHANGED
@@ -1,7 +1,6 @@
1
- import { jsDocComment, objectKey, } from "typespec-http-zod";
1
+ import { jsDocComment, mediaTypeTypeOf, literal, objectKey, statusCodesOf, } from "typespec-http-zod";
2
2
  import { annotationsFor } from "./annotations.js";
3
- import { bodyKindsOf, bodyServingOf, isJsonMediaType, isMediaRange, membersOf } from "./bodies.js";
4
- import { CONTENTLESS_STATUS_CODES, STATUS_GROUPS } from "./statuses.js";
3
+ import { bodyKindsOf, isJsonMediaType, membersOf } from "./bodies.js";
5
4
  /**
6
5
  * **Rendering lives here, with everything else that produces text.**
7
6
  *
@@ -15,7 +14,7 @@ import { CONTENTLESS_STATUS_CODES, STATUS_GROUPS } from "./statuses.js";
15
14
  function renderSecurity(requirements) {
16
15
  return `[${requirements
17
16
  .map((requirement) => `{ ${Object.entries(requirement)
18
- .map(([scheme, scopes]) => `${JSON.stringify(scheme)}: [${scopes.map((s) => JSON.stringify(s)).join(", ")}]`)
17
+ .map(([scheme, scopes]) => `${literal(scheme)}: [${scopes.map((s) => literal(s)).join(", ")}]`)
19
18
  .join(", ")} }`)
20
19
  .join(", ")}]`;
21
20
  }
@@ -39,10 +38,11 @@ const FIELDS_HELPER = `/**
39
38
  * **Zod 4 infers \`Record<string, never>\` for \`z.object({})\`.** That is a fair reading on its own -
40
39
  * no string key may exist - and lethal in an intersection: \`{ id: string } & Record<string, never>\`
41
40
  * makes \`id\` into \`string & never\`, so the input type is uninhabitable and the call site below
42
- * cannot compile. An empty model contributes nothing to an operation's input, and \`unknown\` is the
43
- * identity of \`&\`, so that is what it becomes.
41
+ * cannot compile. An empty model contributes nothing to an operation's input, so it becomes \`{}\`,
42
+ * the identity of \`&\` over the object types an input is made of - not \`unknown\`, which an input
43
+ * made of nothing else would then BE, and which the contract types' \`{}\` does not accept.
44
44
  */
45
- type Fields<T> = string extends keyof T ? ([T[string]] extends [never] ? unknown : T) : T;
45
+ type Fields<T> = string extends keyof T ? ([T[string]] extends [never] ? {} : T) : T;
46
46
 
47
47
  `;
48
48
  const PRODUCED_HELPER = `/**
@@ -51,8 +51,11 @@ const PRODUCED_HELPER = `/**
51
51
  * An index signature and \`readonly\` are true of an inferred type without being obligations on a
52
52
  * producer, so both are dropped here - only where the shape has declared keys beside it, because an
53
53
  * index signature also arrives as a property's own declared type, where stripping it would destroy
54
- * the property. An explicit \`| undefined\` on an optional property is deliberately NOT removed:
55
- * dropping an index signature makes MORE values assignable, and removing \`| undefined\` makes fewer.
54
+ * the property.
55
+ *
56
+ * It is applied to the schema's INPUT, \`z.input\`, because that is what a result is checked against:
57
+ * a property with a default may be left out, and an optional property is \`?: T\`, because
58
+ * \`.exactOptional()\` refuses an explicit \`undefined\`. Both are what the contract types state.
56
59
  */
57
60
  type ProducedKeyOf<T> = keyof {
58
61
  [K in keyof T as string extends K
@@ -110,7 +113,14 @@ export const DEFAULT_RUNTIME_MODULE = "./mcp-runtime.gen.js";
110
113
  */
111
114
  function intersectedValidators(entry) {
112
115
  const { names, route } = entry;
113
- const groups = [names.path, names.query, names.header, names.body];
116
+ /**
117
+ * **A cookie is merged like a header, and for the same reason.** A tool call carries neither, so
118
+ * neither is transport the tool can set - both are inputs the operation needs, and a tool's
119
+ * arguments are one object merged from every location the document declares. The alternative was
120
+ * to refuse the operation, which would remove a working tool over a distinction the MCP surface
121
+ * does not have.
122
+ */
123
+ const groups = [names.path, names.query, names.header, names.cookie, names.body];
114
124
  return groups.filter((name) => name !== undefined && (route.bodyProperty === undefined || name !== names.body));
115
125
  }
116
126
  /**
@@ -130,12 +140,30 @@ function shapedValidators(entry) {
130
140
  function inputTypeOf(entry) {
131
141
  const parts = intersectedValidators(entry).map((name) => `Fields<z.infer<typeof ${name}>>`);
132
142
  if (entry.route.bodyProperty !== undefined && entry.names.body !== undefined) {
143
+ // `?: T`, as the sibling server types it: an absent body is an absent key.
133
144
  const optionalMark = entry.route.optionalBody ? "?" : "";
134
- const optionalValue = entry.route.optionalBody ? " | undefined" : "";
135
- parts.push(`{ ${objectKey(entry.route.bodyProperty)}${optionalMark}: z.infer<typeof ${entry.names.body}>${optionalValue} }`);
145
+ parts.push(`{ ${objectKey(entry.route.bodyProperty)}${optionalMark}: z.infer<typeof ${entry.names.body}> }`);
136
146
  }
147
+ const selected = selectedAcceptOf(entry);
148
+ if (selected !== undefined)
149
+ parts.push(`{ ${selected} }`);
137
150
  return parts.length === 0 ? EMPTY_INPUT : parts.join(" & ");
138
151
  }
152
+ /**
153
+ * **The `accept` literal a tool supplies itself**, on a route several operations share, as
154
+ * `key: "literal"`.
155
+ *
156
+ * The server negotiates which operation answers, so its handler receives the one literal that reached
157
+ * it, and the header validator leaves `accept` out. A tool IS one of those operations, so the literal
158
+ * is known exactly here too: the handler receives it, and the caller is never asked for a value with
159
+ * one answer. It was missing, so a handler typed for the server could not be registered as a tool.
160
+ */
161
+ function selectedAcceptOf(entry) {
162
+ const selected = entry.names.selectedAccept;
163
+ return selected === undefined
164
+ ? undefined
165
+ : `${objectKey(selected.name)}: ${literal(selected.value)}`;
166
+ }
139
167
  /**
140
168
  * The tool's ARGUMENT schema: every group the document declares, merged into one object.
141
169
  *
@@ -154,65 +182,46 @@ function inputTypeOf(entry) {
154
182
  function inputSchemaOf(entry, sealed) {
155
183
  const constructor = sealed ? "z.strictObject" : "z.looseObject";
156
184
  const spreads = shapedValidators(entry).map((name) => `...${name}.shape`);
185
+ /**
186
+ * **A decoded path parameter takes its VALUE schema, not the path validator's field.** That field
187
+ * is `z.preprocess(<decode>, <value schema>)`: it reads the segment TEXT a router extracts -
188
+ * `primitive.a` for `{.param}` - and hands on `a`. A tool argument is already the value, so through
189
+ * the decoder `{ param: "a" }` was refused as not the expansion the route declares, while
190
+ * `tools/list` published `param` as a plain string, and a value starting with the route's literal
191
+ * was rewritten without an error (issue #1). Six `@typespec/http-specs` path scenarios failed.
192
+ *
193
+ * Which parameters are decoded is the library's fact, `decodedPathParameters`, read rather than
194
+ * re-derived, so this cannot drift from the validator it overrides. The override comes after the
195
+ * spread, so it wins.
196
+ */
197
+ const path = entry.names.path;
198
+ if (path !== undefined) {
199
+ for (const name of entry.route.decodedPathParameters) {
200
+ spreads.push(`${objectKey(name)}: ${path}.shape[${literal(name)}].out`);
201
+ }
202
+ }
157
203
  if (entry.route.bodyProperty !== undefined && entry.names.body !== undefined) {
158
- spreads.push(`${objectKey(entry.route.bodyProperty)}: ${entry.names.body}`);
204
+ /**
205
+ * **Optional where the document lets the body be absent.** It was required here while the
206
+ * handler's input said \`body?:\`, so a tool call omitting a body the document makes optional
207
+ * was refused before it reached the handler.
208
+ */
209
+ const optional = entry.route.optionalBody ? ".exactOptional()" : "";
210
+ spreads.push(`${objectKey(entry.route.bodyProperty)}: ${entry.names.body}${optional}`);
159
211
  }
160
212
  if (spreads.length === 0)
161
213
  return "z.strictObject({})";
162
214
  return `${constructor}({ ${spreads.join(", ")} })`;
163
215
  }
164
- /** The codes of the status group a range key names. */
165
- function groupCodesOf(status) {
166
- return STATUS_GROUPS[Number(status.charAt(0))].codes;
167
- }
168
- /**
169
- * The type of the `contentType` a handler names for a member: the sibling emitter's rule.
170
- *
171
- * **A range is typed as the types inside it**: `image/*` is `` `image/${string}` ``, so `text/html`
172
- * under it does not compile, and TypeScript still narrows a literal member away from it. Only the
173
- * full range is `string`.
174
- */
175
- function contentTypeTypeOf(mediaType) {
176
- if (!isMediaRange(mediaType))
177
- return JSON.stringify(mediaType);
178
- if (mediaType === "*/*")
179
- return "string";
180
- return `\`${mediaType.slice(0, -1)}\${string}\``;
181
- }
182
216
  /**
183
- * A member's status as a TypeScript type: disjoint from every other member's, by construction.
184
- *
185
- * **Written as a union of literals, never as Hono's type names.** This package's output imports no
186
- * Hono, so where the sibling emitter writes `Exclude<ClientErrorStatusCode, 404 | ContentlessStatusCode>`
187
- * this writes the codes that expression resolves to, from the same sets (`statuses.ts`, held equal to
188
- * Hono's declaration file by test):
189
- *
190
- * - an exact code is itself;
191
- * - a range is its group, less every code the operation declares exactly, and less the statuses that
192
- * cannot carry a body where the range has one;
193
- * - `default` is every official status from 2xx to 5xx, less the exact codes, less every code a
194
- * declared range covers, and less the contentless statuses where it has a body.
217
+ * The status a result member is typed with: the codes its response answers with, written out.
195
218
  *
196
- * **Disjoint is the property that makes a wrong body a compile error**: were `default` allowed to
197
- * overlap a declared status, a 404 carrying the default arm's body would type-check through it.
219
+ * **Written out, never as Hono's group types**, because this file imports nothing from Hono. The
220
+ * codes are the library's `statusCodesOf`, the same rule the server emitter and the contract types
221
+ * read, so a tool's result and a handler's result agree member by member.
198
222
  */
199
223
  function statusTypeOf(response, route) {
200
- const { status } = response;
201
- if (typeof status === "number")
202
- return String(status);
203
- const exact = new Set(route.responses.flatMap((candidate) => typeof candidate.status === "number" ? [candidate.status] : []));
204
- const contentless = new Set(response.schema === undefined ? [] : CONTENTLESS_STATUS_CODES);
205
- const candidates = status === "default"
206
- ? [2, 3, 4, 5].flatMap((digit) => STATUS_GROUPS[digit].codes)
207
- : groupCodesOf(status);
208
- const ranged = new Set(status === "default"
209
- ? route.responses.flatMap((candidate) => typeof candidate.status === "string" && candidate.status !== "default"
210
- ? groupCodesOf(candidate.status)
211
- : [])
212
- : []);
213
- const codes = candidates
214
- .filter((code) => !exact.has(code) && !ranged.has(code) && !contentless.has(code))
215
- .toSorted((a, b) => a - b);
224
+ const codes = statusCodesOf(response, route.responses, (candidate) => candidate.schema !== undefined);
216
225
  return codes.length === 0 ? "never" : codes.join(" | ");
217
226
  }
218
227
  /**
@@ -226,23 +235,28 @@ function resultTypeOf(entry) {
226
235
  const { response } = member;
227
236
  const fields = [`readonly status: ${statusTypeOf(response, entry.route)}`];
228
237
  if (member.choosesMediaType && member.mediaType !== undefined) {
229
- fields.push(`readonly contentType: ${contentTypeTypeOf(member.mediaType)}`);
238
+ fields.push(`readonly contentType: ${mediaTypeTypeOf(member.mediaType)}`);
230
239
  }
231
- switch (bodyServingOf(member)) {
240
+ switch (member.serving) {
232
241
  case "none":
233
242
  fields.push("readonly body?: undefined");
234
243
  break;
235
244
  case "stream":
236
- fields.push("readonly body: ReadableStream");
245
+ fields.push("readonly body: ByteStream");
237
246
  break;
238
247
  case "binary":
239
- fields.push("readonly body: ReadableStream | Uint8Array<ArrayBuffer> | ArrayBuffer");
248
+ /**
249
+ * **`ByteStream`, not `ReadableStream`, so a backend's contract value fits**, as the sibling
250
+ * server types it. The contract types state a binary body structurally, because they cannot
251
+ * name the platform stream; a platform stream still satisfies it.
252
+ */
253
+ fields.push("readonly body: ByteStream | Uint8Array<ArrayBuffer> | ArrayBuffer");
240
254
  break;
241
255
  case "unvalidated":
242
256
  fields.push("readonly body: string");
243
257
  break;
244
258
  default:
245
- fields.push(`readonly body: Produced<z.infer<typeof ${member.schemaName}>>`);
259
+ fields.push(`readonly body: Produced<z.input<typeof ${member.schemaName}>>`);
246
260
  }
247
261
  if (response.headers.length > 0) {
248
262
  /**
@@ -328,7 +342,7 @@ export function renderTools(entries, declaredOperationIds, options) {
328
342
  if (entry.names.body !== undefined)
329
343
  imported.add(entry.names.body);
330
344
  for (const member of membersOf(entry.route, entry.names)) {
331
- const serving = bodyServingOf(member);
345
+ const serving = member.serving;
332
346
  if ((serving === "json" || serving === "text") && member.schemaName !== undefined) {
333
347
  imported.add(member.schemaName);
334
348
  }
@@ -349,7 +363,13 @@ export function renderTools(entries, declaredOperationIds, options) {
349
363
  const runtimeTypes = ["McpServer", "ServerContext", "ToolDeps"];
350
364
  if (entries.length > 0)
351
365
  runtimeTypes.push("Awaitable");
352
- const from = JSON.stringify(DEFAULT_RUNTIME_MODULE);
366
+ const usesByteStream = entries.some((entry) => membersOf(entry.route, entry.names).some((member) => {
367
+ const serving = member.serving;
368
+ return serving === "binary" || serving === "stream";
369
+ }));
370
+ if (usesByteStream)
371
+ runtimeTypes.push("ByteStream");
372
+ const from = literal(DEFAULT_RUNTIME_MODULE);
353
373
  const runtimeImport = [
354
374
  ...(entries.length > 0 ? [`import { toolResult } from ${from};`] : []),
355
375
  `import type { ${runtimeTypes.toSorted().join(", ")} } from ${from};`,
@@ -364,7 +384,7 @@ export function renderTools(entries, declaredOperationIds, options) {
364
384
  */
365
385
  const usesFields = entries.some((entry) => intersectedValidators(entry).length > 0);
366
386
  const usesProduced = entries.some((entry) => membersOf(entry.route, entry.names).some((member) => {
367
- const serving = bodyServingOf(member);
387
+ const serving = member.serving;
368
388
  return serving === "json" || serving === "text";
369
389
  }));
370
390
  const helpers = `${usesFields ? FIELDS_HELPER : ""}${usesProduced ? PRODUCED_HELPER : ""}`;
@@ -394,10 +414,10 @@ export function renderTools(entries, declaredOperationIds, options) {
394
414
  .join("\n");
395
415
  /** Every scope any selected operation demands, de-duplicated and ordered. */
396
416
  const scopes = [...new Set(entries.flatMap((entry) => [...entry.route.scopes]))].toSorted();
397
- const scopesLiteral = `[${scopes.map((scope) => JSON.stringify(scope)).join(", ")}]`;
417
+ const scopesLiteral = `[${scopes.map((scope) => literal(scope)).join(", ")}]`;
398
418
  const declaredUnion = declaredOperationIds.length === 0
399
419
  ? "never"
400
- : declaredOperationIds.map((id) => JSON.stringify(id)).join(" | ");
420
+ : declaredOperationIds.map((id) => literal(id)).join(" | ");
401
421
  /**
402
422
  * **Registered in a deterministic order, sorted by tool name.**
403
423
  *
@@ -408,20 +428,26 @@ export function renderTools(entries, declaredOperationIds, options) {
408
428
  const ordered = [...entries].toSorted((a, b) => a.toolName.localeCompare(b.toolName));
409
429
  const registrations = ordered
410
430
  .map((entry) => {
411
- const title = entry.title === undefined ? "" : `\n\t\t\t\ttitle: ${JSON.stringify(entry.title)},`;
431
+ const title = entry.title === undefined ? "" : `\n\t\t\t\ttitle: ${literal(entry.title)},`;
412
432
  const description = entry.description === undefined
413
433
  ? ""
414
- : `\n\t\t\t\tdescription: ${JSON.stringify(entry.description)},`;
434
+ : `\n\t\t\t\tdescription: ${literal(entry.description)},`;
415
435
  const published = outputSchemaOf(entry);
416
436
  const outputSchema = published === undefined ? "" : `\n\t\t\t\toutputSchema: ${published},`;
417
437
  /**
418
438
  * The body kinds the runtime cannot read off an arm, only where one applies: a JSON body is
419
439
  * known from its media type, and most operations answer nothing else.
420
440
  */
421
- const kinds = bodyKindsOf(entry.route);
422
- const bodies = kinds.length === 0
441
+ const kinds = new Map();
442
+ for (const [status, mediaType, kind] of bodyKindsOf(entry.route, entry.names)) {
443
+ kinds.set(status, [
444
+ ...(kinds.get(status) ?? []),
445
+ `${literal(mediaType)}: ${literal(kind)}`,
446
+ ]);
447
+ }
448
+ const bodies = kinds.size === 0
423
449
  ? ""
424
- : `, bodies: { ${kinds.map(([status, kind]) => `${JSON.stringify(status)}: ${JSON.stringify(kind)}`).join(", ")} }`;
450
+ : `, bodies: { ${[...kinds].map(([status, byType]) => `${literal(status)}: { ${byType.join(", ")} }`).join(", ")} }`;
425
451
  const declared = `{ arms: ${entry.names.responses}, outputSchema: ${published ?? "undefined"}${bodies} }`;
426
452
  /**
427
453
  * **A parameterless tool must not DECLARE an argument it never reads.**
@@ -432,27 +458,33 @@ export function renderTools(entries, declaredOperationIds, options) {
432
458
  * omission is deliberate.
433
459
  */
434
460
  const parameterless = intersectedValidators(entry).length === 0 && entry.route.bodyProperty === undefined;
435
- const argument = parameterless ? "{}" : "{ ...args }";
461
+ // The selected `accept` last, so a caller's own `accept` argument cannot replace it.
462
+ const supplied = selectedAcceptOf(entry);
463
+ const fields = [
464
+ ...(parameterless ? [] : ["...args"]),
465
+ ...(supplied === undefined ? [] : [supplied]),
466
+ ];
467
+ const argument = fields.length === 0 ? "{}" : `{ ${fields.join(", ")} }`;
436
468
  const argumentName = parameterless ? "_args" : "args";
437
469
  return ` {
438
470
  const tool = server.registerTool(
439
- ${JSON.stringify(entry.toolName)},
471
+ ${literal(entry.toolName)},
440
472
  {${title}${description}
441
473
  inputSchema: ${inputSchemaName(entry)},${outputSchema}
442
474
  annotations: ${renderAnnotations(annotationsFor(entry.route.verb))},
443
475
  },
444
476
  async (${argumentName}, context) => {
445
- const ctx = deps.context(context, ${JSON.stringify(entry.route.authentication)});
477
+ const ctx = deps.context(context, ${literal(entry.route.authentication)});
446
478
  if (ctx === null) return deps.noContext(context);
447
479
  return toolResult(
448
480
  ${declared},
449
- ${JSON.stringify(entry.route.operationId)},
481
+ ${literal(entry.route.operationId)},
450
482
  await handlersFor(context).${entry.route.operationId}(ctx, ${argument}),
451
483
  );
452
484
  },
453
485
  );
454
486
  if (deps.permits(${renderSecurity(entry.requirements)})) {
455
- registered.push(${JSON.stringify(entry.toolName)});
487
+ registered.push(${literal(entry.toolName)});
456
488
  } else {
457
489
  tool.remove();
458
490
  }
@@ -517,7 +549,7 @@ export type ToolsExhaustive<T> = T & Record<Exclude<keyof T, DeclaredOperationId
517
549
  export const SCOPES_SUPPORTED = ${scopesLiteral} as const;
518
550
 
519
551
  /** Every tool this file registers, in the order it registers them. */
520
- export const TOOL_NAMES = [${ordered.map((entry) => JSON.stringify(entry.toolName)).join(", ")}] as const;
552
+ export const TOOL_NAMES = [${ordered.map((entry) => literal(entry.toolName)).join(", ")}] as const;
521
553
 
522
554
  /**
523
555
  * Register every tool this service declares, onto a server the APPLICATION owns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeSpec emitter: generate an MCP tool surface, and the Zod validators it publishes, from an HTTP service definition, dispatching into the same handlers a generated HTTP server calls.",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -45,29 +45,29 @@
45
45
  "provenance": true
46
46
  },
47
47
  "dependencies": {
48
- "typespec-http-zod": "^0.26.0"
48
+ "typespec-http-zod": "^0.29.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@hono/zod-validator": "^0.9.1",
52
52
  "@modelcontextprotocol/client": "2.0.0",
53
53
  "@modelcontextprotocol/server": "2.0.0",
54
54
  "@types/node": "^26.5.1",
55
- "@typespec/compiler": "1.16.0",
56
- "@typespec/events": "0.86.0",
57
- "@typespec/http": "1.16.0",
58
- "@typespec/http-specs": "0.1.0-alpha.43",
59
- "@typespec/openapi": "1.16.0",
60
- "@typespec/openapi3": "1.16.0",
61
- "@typespec/rest": "0.86.0",
62
- "@typespec/sse": "0.86.0",
63
- "@typespec/streams": "0.86.0",
64
- "@typespec/versioning": "0.86.0",
65
- "@typespec/xml": "0.86.0",
55
+ "@typespec/compiler": "1.17.0",
56
+ "@typespec/events": "0.87.0",
57
+ "@typespec/http": "1.17.0",
58
+ "@typespec/http-specs": "0.1.0-alpha.44",
59
+ "@typespec/openapi": "1.17.0",
60
+ "@typespec/openapi3": "1.17.0",
61
+ "@typespec/rest": "0.87.0",
62
+ "@typespec/sse": "0.87.0",
63
+ "@typespec/streams": "0.87.0",
64
+ "@typespec/versioning": "0.87.0",
65
+ "@typespec/xml": "0.87.0",
66
66
  "hono": "^4.13.7",
67
67
  "oxfmt": "^0.67.0",
68
68
  "oxlint": "^1.82.0",
69
69
  "typescript": "~7.0.2",
70
- "typespec-hono": "0.23.0",
70
+ "typespec-hono": "0.26.0",
71
71
  "typespec-http-mcp": "link:.",
72
72
  "vitest": "^5.0.0",
73
73
  "wrangler": "^4.131.1",
@@ -77,6 +77,7 @@
77
77
  "@modelcontextprotocol/server": "^2.0.0",
78
78
  "@typespec/compiler": "^1.15.0",
79
79
  "@typespec/http": "^1.15.0",
80
+ "@typespec/openapi": "^1.15.0",
80
81
  "zod": "^4.5.0"
81
82
  },
82
83
  "engines": {
@@ -84,7 +85,7 @@
84
85
  },
85
86
  "tspMain": "lib/main.tsp",
86
87
  "scripts": {
87
- "build": "tsc -p tsconfig.build.json",
88
+ "build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
88
89
  "test": "tsc -p tsconfig.build.json && vitest run",
89
90
  "test:unit": "tsc -p tsconfig.build.json && vitest run --project unit",
90
91
  "typecheck": "tsc -p tsconfig.json",