typespec-http-mcp 0.2.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.
- package/dist/src/tools.js +33 -15
- package/package.json +3 -3
package/dist/src/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsDocComment, mediaTypeTypeOf, objectKey, statusCodesOf, } from "typespec-http-zod";
|
|
1
|
+
import { jsDocComment, mediaTypeTypeOf, literal, objectKey, statusCodesOf, } from "typespec-http-zod";
|
|
2
2
|
import { annotationsFor } from "./annotations.js";
|
|
3
3
|
import { bodyKindsOf, isJsonMediaType, membersOf } from "./bodies.js";
|
|
4
4
|
/**
|
|
@@ -14,7 +14,7 @@ import { bodyKindsOf, isJsonMediaType, membersOf } from "./bodies.js";
|
|
|
14
14
|
function renderSecurity(requirements) {
|
|
15
15
|
return `[${requirements
|
|
16
16
|
.map((requirement) => `{ ${Object.entries(requirement)
|
|
17
|
-
.map(([scheme, scopes]) => `${
|
|
17
|
+
.map(([scheme, scopes]) => `${literal(scheme)}: [${scopes.map((s) => literal(s)).join(", ")}]`)
|
|
18
18
|
.join(", ")} }`)
|
|
19
19
|
.join(", ")}]`;
|
|
20
20
|
}
|
|
@@ -162,7 +162,7 @@ function selectedAcceptOf(entry) {
|
|
|
162
162
|
const selected = entry.names.selectedAccept;
|
|
163
163
|
return selected === undefined
|
|
164
164
|
? undefined
|
|
165
|
-
: `${objectKey(selected.name)}: ${
|
|
165
|
+
: `${objectKey(selected.name)}: ${literal(selected.value)}`;
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
168
|
* The tool's ARGUMENT schema: every group the document declares, merged into one object.
|
|
@@ -182,6 +182,24 @@ function selectedAcceptOf(entry) {
|
|
|
182
182
|
function inputSchemaOf(entry, sealed) {
|
|
183
183
|
const constructor = sealed ? "z.strictObject" : "z.looseObject";
|
|
184
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
|
+
}
|
|
185
203
|
if (entry.route.bodyProperty !== undefined && entry.names.body !== undefined) {
|
|
186
204
|
/**
|
|
187
205
|
* **Optional where the document lets the body be absent.** It was required here while the
|
|
@@ -351,7 +369,7 @@ export function renderTools(entries, declaredOperationIds, options) {
|
|
|
351
369
|
}));
|
|
352
370
|
if (usesByteStream)
|
|
353
371
|
runtimeTypes.push("ByteStream");
|
|
354
|
-
const from =
|
|
372
|
+
const from = literal(DEFAULT_RUNTIME_MODULE);
|
|
355
373
|
const runtimeImport = [
|
|
356
374
|
...(entries.length > 0 ? [`import { toolResult } from ${from};`] : []),
|
|
357
375
|
`import type { ${runtimeTypes.toSorted().join(", ")} } from ${from};`,
|
|
@@ -396,10 +414,10 @@ export function renderTools(entries, declaredOperationIds, options) {
|
|
|
396
414
|
.join("\n");
|
|
397
415
|
/** Every scope any selected operation demands, de-duplicated and ordered. */
|
|
398
416
|
const scopes = [...new Set(entries.flatMap((entry) => [...entry.route.scopes]))].toSorted();
|
|
399
|
-
const scopesLiteral = `[${scopes.map((scope) =>
|
|
417
|
+
const scopesLiteral = `[${scopes.map((scope) => literal(scope)).join(", ")}]`;
|
|
400
418
|
const declaredUnion = declaredOperationIds.length === 0
|
|
401
419
|
? "never"
|
|
402
|
-
: declaredOperationIds.map((id) =>
|
|
420
|
+
: declaredOperationIds.map((id) => literal(id)).join(" | ");
|
|
403
421
|
/**
|
|
404
422
|
* **Registered in a deterministic order, sorted by tool name.**
|
|
405
423
|
*
|
|
@@ -410,10 +428,10 @@ export function renderTools(entries, declaredOperationIds, options) {
|
|
|
410
428
|
const ordered = [...entries].toSorted((a, b) => a.toolName.localeCompare(b.toolName));
|
|
411
429
|
const registrations = ordered
|
|
412
430
|
.map((entry) => {
|
|
413
|
-
const title = entry.title === undefined ? "" : `\n\t\t\t\ttitle: ${
|
|
431
|
+
const title = entry.title === undefined ? "" : `\n\t\t\t\ttitle: ${literal(entry.title)},`;
|
|
414
432
|
const description = entry.description === undefined
|
|
415
433
|
? ""
|
|
416
|
-
: `\n\t\t\t\tdescription: ${
|
|
434
|
+
: `\n\t\t\t\tdescription: ${literal(entry.description)},`;
|
|
417
435
|
const published = outputSchemaOf(entry);
|
|
418
436
|
const outputSchema = published === undefined ? "" : `\n\t\t\t\toutputSchema: ${published},`;
|
|
419
437
|
/**
|
|
@@ -424,12 +442,12 @@ export function renderTools(entries, declaredOperationIds, options) {
|
|
|
424
442
|
for (const [status, mediaType, kind] of bodyKindsOf(entry.route, entry.names)) {
|
|
425
443
|
kinds.set(status, [
|
|
426
444
|
...(kinds.get(status) ?? []),
|
|
427
|
-
`${
|
|
445
|
+
`${literal(mediaType)}: ${literal(kind)}`,
|
|
428
446
|
]);
|
|
429
447
|
}
|
|
430
448
|
const bodies = kinds.size === 0
|
|
431
449
|
? ""
|
|
432
|
-
: `, bodies: { ${[...kinds].map(([status, byType]) => `${
|
|
450
|
+
: `, bodies: { ${[...kinds].map(([status, byType]) => `${literal(status)}: { ${byType.join(", ")} }`).join(", ")} }`;
|
|
433
451
|
const declared = `{ arms: ${entry.names.responses}, outputSchema: ${published ?? "undefined"}${bodies} }`;
|
|
434
452
|
/**
|
|
435
453
|
* **A parameterless tool must not DECLARE an argument it never reads.**
|
|
@@ -450,23 +468,23 @@ export function renderTools(entries, declaredOperationIds, options) {
|
|
|
450
468
|
const argumentName = parameterless ? "_args" : "args";
|
|
451
469
|
return ` {
|
|
452
470
|
const tool = server.registerTool(
|
|
453
|
-
${
|
|
471
|
+
${literal(entry.toolName)},
|
|
454
472
|
{${title}${description}
|
|
455
473
|
inputSchema: ${inputSchemaName(entry)},${outputSchema}
|
|
456
474
|
annotations: ${renderAnnotations(annotationsFor(entry.route.verb))},
|
|
457
475
|
},
|
|
458
476
|
async (${argumentName}, context) => {
|
|
459
|
-
const ctx = deps.context(context, ${
|
|
477
|
+
const ctx = deps.context(context, ${literal(entry.route.authentication)});
|
|
460
478
|
if (ctx === null) return deps.noContext(context);
|
|
461
479
|
return toolResult(
|
|
462
480
|
${declared},
|
|
463
|
-
${
|
|
481
|
+
${literal(entry.route.operationId)},
|
|
464
482
|
await handlersFor(context).${entry.route.operationId}(ctx, ${argument}),
|
|
465
483
|
);
|
|
466
484
|
},
|
|
467
485
|
);
|
|
468
486
|
if (deps.permits(${renderSecurity(entry.requirements)})) {
|
|
469
|
-
registered.push(${
|
|
487
|
+
registered.push(${literal(entry.toolName)});
|
|
470
488
|
} else {
|
|
471
489
|
tool.remove();
|
|
472
490
|
}
|
|
@@ -531,7 +549,7 @@ export type ToolsExhaustive<T> = T & Record<Exclude<keyof T, DeclaredOperationId
|
|
|
531
549
|
export const SCOPES_SUPPORTED = ${scopesLiteral} as const;
|
|
532
550
|
|
|
533
551
|
/** Every tool this file registers, in the order it registers them. */
|
|
534
|
-
export const TOOL_NAMES = [${ordered.map((entry) =>
|
|
552
|
+
export const TOOL_NAMES = [${ordered.map((entry) => literal(entry.toolName)).join(", ")}] as const;
|
|
535
553
|
|
|
536
554
|
/**
|
|
537
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.
|
|
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,7 +45,7 @@
|
|
|
45
45
|
"provenance": true
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"typespec-http-zod": "^0.
|
|
48
|
+
"typespec-http-zod": "^0.29.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@hono/zod-validator": "^0.9.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"oxfmt": "^0.67.0",
|
|
68
68
|
"oxlint": "^1.82.0",
|
|
69
69
|
"typescript": "~7.0.2",
|
|
70
|
-
"typespec-hono": "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",
|