typespec-http-mcp 0.3.0 → 0.5.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/lib.d.ts CHANGED
@@ -75,6 +75,9 @@ type Diagnostics = {
75
75
  "tool-name-collision": {
76
76
  readonly default: CallableMessage<["first", "second", "name"]>;
77
77
  };
78
+ "shared-input-key": {
79
+ readonly default: CallableMessage<["operationId", "types"]>;
80
+ };
78
81
  "binary-tool-input": {
79
82
  readonly default: CallableMessage<["operationId", "types"]>;
80
83
  };
package/dist/src/lib.js CHANGED
@@ -128,6 +128,23 @@ const diagnostics = {
128
128
  * contract no reader of the document could predict, and would put a whole upload through a model's
129
129
  * context window.
130
130
  */
131
+ /**
132
+ * **An operation two of whose inputs would arrive as one argument is not a tool.**
133
+ *
134
+ * A tool's arguments are the path, query, header and cookie groups spread into one object, the
135
+ * body merged or named beside them - the same input a server's handler receives. `@header("x")`
136
+ * beside `@query("x")` therefore published one `x` argument, the later group's schema replacing
137
+ * the earlier, and the handler was handed one value for two declarations (typespec-http-graphql,
138
+ * 2026-10-16). `typespec-http-zod` names the pair in `EmittedRoute.sharedInputKeys` and reports it
139
+ * as its own `shared-input-key`; this is the tool surface declining it, as `typespec-hono` declines
140
+ * to mount it.
141
+ */
142
+ "shared-input-key": {
143
+ severity: "warning",
144
+ messages: {
145
+ default: paramMessage `'${"operationId"}' is not a tool: ${"types"} would arrive as one argument, which holds one value. Give one of them a different name, or exclude the operation with 'exclude-tags'.`,
146
+ },
147
+ },
131
148
  "binary-tool-input": {
132
149
  severity: "warning",
133
150
  messages: {
@@ -2,6 +2,9 @@ import { type Program } from "@typespec/compiler";
2
2
  import type { HttpOperation } from "@typespec/http";
3
3
  import type { EmittedRoute, RouteSchemaNames } from "typespec-http-zod";
4
4
  export type Refusal = {
5
+ readonly kind: "shared-input-key";
6
+ readonly types: string;
7
+ } | {
5
8
  readonly kind: "binary-tool-input";
6
9
  readonly types: string;
7
10
  } | {
@@ -8,6 +8,14 @@ import { unvalidatedResponsesOf } from "./bodies.js";
8
8
  * arithmetically impossible. A WARNING leaves the tool in place and says something about its quality.
9
9
  * Conflating them would let a quality complaint quietly delete a tool.
10
10
  */
11
+ /** How a `shared-input-key` refusal names where each of the two inputs comes from. */
12
+ const INPUT_LOCATION_NOUNS = {
13
+ path: "path parameter",
14
+ query: "query parameter",
15
+ header: "header",
16
+ cookie: "cookie",
17
+ body: "body",
18
+ };
11
19
  /** A media type a tool can actually send. `application/json`, and the `+json` structured suffix. */
12
20
  function isJson(contentType) {
13
21
  const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
@@ -37,9 +45,10 @@ function isMergeableBody(program, type) {
37
45
  if (type.indexer?.key?.name === "integer")
38
46
  return false;
39
47
  /**
40
- * A model carrying `@discriminator` is emitted as a UNION of its variants, not as an object --
41
- * measured: `z.discriminatedUnion("kind", [...])`, which has no shape. Which variant arrived is
42
- * decided by a value, so there is no single set of properties to merge into the arguments.
48
+ * A model carrying `@discriminator` is emitted as a UNION of its variants and itself, not as an
49
+ * object -- measured: `z.union([z.discriminatedUnion("kind", [...]), z.object(...)])`, which has
50
+ * no shape. Which arm a value takes is decided by the value, so there is no single set of
51
+ * properties to merge into the arguments.
43
52
  */
44
53
  if (getDiscriminator(program, type) !== undefined)
45
54
  return false;
@@ -70,6 +79,21 @@ function isMergeableBody(program, type) {
70
79
  */
71
80
  export function refusalFor(program, route, operation) {
72
81
  const types = route.requestContentTypes;
82
+ /**
83
+ * **Two inputs under one key cannot both be arguments.** A tool's arguments are the parameter
84
+ * groups spread into one object, as a server's handler input is, so `@header("x")` beside
85
+ * `@query("x")` published one `x` whose later schema replaced the earlier, and the handler was
86
+ * handed one value for two declarations. The library names every such pair; the operation is not
87
+ * a tool, for the reason `typespec-hono` does not mount it.
88
+ */
89
+ if (route.sharedInputKeys.length > 0) {
90
+ return {
91
+ kind: "shared-input-key",
92
+ types: route.sharedInputKeys
93
+ .map((shared) => `the ${INPUT_LOCATION_NOUNS[shared.first]} '${shared.key}' and the ${INPUT_LOCATION_NOUNS[shared.second]} '${shared.key}'`)
94
+ .join("; "),
95
+ };
96
+ }
73
97
  /**
74
98
  * A raw body is bytes or a stream; a multipart body is parts, one of which is usually a file.
75
99
  * Neither is a JSON value, and encoding one into a string argument would put an upload through a
@@ -101,7 +125,13 @@ export function refusalFor(program, route, operation) {
101
125
  * body is merged into the argument object, so it must have properties to merge.
102
126
  */
103
127
  if (route.bodyProperty === undefined) {
104
- const body = operation.parameters.body?.type;
128
+ /**
129
+ * **A body declared `void` is no body**, and the library lists no media type for one; read
130
+ * that way, there is nothing to merge. Read off the operation, its intrinsic type is not an
131
+ * object and the tool was refused as `non-object-body`. A body declared `never` the library
132
+ * refuses, as `never-body`.
133
+ */
134
+ const body = types.length === 0 ? undefined : operation.parameters.body?.type;
105
135
  if (!isMergeableBody(program, body)) {
106
136
  return { kind: "non-object-body", types: body?.kind ?? "unknown" };
107
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.29.0"
48
+ "typespec-http-zod": "^0.31.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@hono/zod-validator": "^0.9.1",
@@ -66,8 +66,9 @@
66
66
  "hono": "^4.13.7",
67
67
  "oxfmt": "^0.67.0",
68
68
  "oxlint": "^1.82.0",
69
+ "secure-json-parse": "4.1.1",
69
70
  "typescript": "~7.0.2",
70
- "typespec-hono": "0.26.0",
71
+ "typespec-hono": "0.28.0",
71
72
  "typespec-http-mcp": "link:.",
72
73
  "vitest": "^5.0.0",
73
74
  "wrangler": "^4.131.1",