@webpieces/mcp-server 0.4.795 → 0.4.796

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/README.md CHANGED
@@ -25,13 +25,11 @@ class FindOrderResponse {
25
25
  abstract class OrdersApi {
26
26
  @WpMcpAuthJwt({ allRolesAllowed: true })
27
27
  @WpAuthJwt({ allRolesAllowed: true })
28
- @Endpoint('/find', 'rpc')
28
+ @Endpoint(POST, '/find', READ, RPC)
29
29
  @WpResponseDto(() => FindOrderResponse)
30
30
  @WpMcpTool({
31
31
  name: 'orders_find',
32
32
  description: 'Find one order owned by the signed-in user.',
33
- readOnlyHint: true,
34
- idempotentHint: true,
35
33
  openWorldHint: false,
36
34
  })
37
35
  find(request: FindOrderRequest): Promise<FindOrderResponse> {
@@ -44,6 +42,12 @@ The `@WpMcpTool.description` becomes the tool description returned by `tools/lis
44
42
  response classes generate `inputSchema` and `outputSchema`; `@WpDtoField` supplies property
45
43
  descriptions and the facts TypeScript erases, such as optionality and array element types.
46
44
 
45
+ Every `@Endpoint` declares one enum-backed operation: `READ`, `WRITE_IDEMPOTENT`, or `WRITE`.
46
+ This is independent of its required `GET` or `POST` argument: a GET may deliberately write and a
47
+ POST may be a read. MCP derives
48
+ `readOnlyHint`, `idempotentHint`, and `destructiveHint` from that one declaration; `@WpMcpTool`
49
+ owns only the stable name, description, and `openWorldHint`.
50
+
47
51
  ### Typed maps
48
52
 
49
53
  A `Record<string, V>` reflects as plain `Object`, so its value type must be declared — the same reason
@@ -220,12 +224,18 @@ the request, the identity and the timing on it, and none of the barer second lin
220
224
  so none of the three methods takes a correlation parameter — the deleted `McpFailureScope` was a copy
221
225
  of context the renderers can already read.
222
226
 
223
- What the model reads inside an `isError` result: an `ApiEndUserError` message verbatim plus
224
- `errorCode` (it survives the remote hop byte-for-byte); an `ApiBadRequestError`'s `callerMessage` and
225
- `field`, never its operator `message`; for an implementation failure, generic text naming the tool and
226
- requestId and saying it is a bug in the tool, not in the arguments; for every other kind, that kind's
227
- generic message plus `retryAfterSeconds` where present, and for an `ApiCodedError` its
228
- `statusCode` and `errorCode`.
227
+ The default `content[0].text` is JSON with an explicit `{ "error": { ... } }` envelope, because some
228
+ clients display only content and hide the top-level MCP `isError`. The nested error carries the
229
+ precise `kind`, a small ownership `category`, explicit `retry` guidance, a safe message, and the
230
+ `requestId`. It may also carry safe fields such as `field`, `callerMessage`, `errorCode`,
231
+ `retryAfterSeconds`, and `statusCode`. It never repeats `isError` inside the content.
232
+
233
+ Implementation failures are `category: "bug", retry: "never"` and tell the caller to give the
234
+ request ID to support; operator details remain redacted. Generic dependency failures are also
235
+ non-retryable. Specific gateway, timeout, unavailable, throttling, and backoff failures are
236
+ temporary, but a retry is described as safe only for a `read` or `write-idempotent` endpoint. A
237
+ `write` reports `retry: "unsafe-outcome-unknown"`, because a lost response does not prove the write
238
+ failed to commit.
229
239
 
230
240
  Every reply carries the requestId so a user can quote it: `_meta["webpieces/requestId"]` on every
231
241
  `tools/call` result (success or `isError`), `requestId` in the `isError` payload, and
@@ -273,11 +283,10 @@ class LangMcpErrorTranslator implements McpErrorTranslator {
273
283
  on for OAuth discovery, and an app rewriting it breaks connector onboarding in a way that is
274
284
  extremely hard to debug.
275
285
 
276
- A remote binding's generated Node client turns a dependency's 4xx into the gateway's own
277
- `ApiImplementationError`, and a 5xx into an `ApiDependencyError` (the uniform rule in core-util's
278
- `ReceivedApiErrorRule`). A gateway that wants the model to see the peer's typed failure instead
279
- registers an `ErrorTranslator` via `ClientRegistry.setErrorTranslator(...)` whose `fromWire` THROWS
280
- the decoded Webpieces payload; with it, local and remote bindings produce identical tool results.
286
+ A remote binding's generated client preserves typed dependency gateway, unavailable, timeout, and
287
+ backoff failures. Foreign/bodyless responses are classified by status: 408/504 timeout, 429 backoff,
288
+ 502 bad gateway, and 503 unavailable (or backoff when `Retry-After` is present). Other 4xx remains
289
+ the caller service's `ApiImplementationError`; other 5xx remains generic `ApiDependencyError`.
281
290
 
282
291
  ### Why there is no `fromWire` on `McpErrorTranslator`
283
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.795",
3
+ "version": "0.4.796",
4
4
  "description": "Secure MCP tool generation from annotated Webpieces API contracts",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -24,15 +24,15 @@
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/node": "2.0.0",
26
26
  "@modelcontextprotocol/server": "2.0.0",
27
- "@webpieces/core-context": "0.4.795",
28
- "@webpieces/core-util": "0.4.795",
29
- "@webpieces/http-routing": "0.4.795",
30
- "@webpieces/http-server": "0.4.795",
27
+ "@webpieces/core-context": "0.4.796",
28
+ "@webpieces/core-util": "0.4.796",
29
+ "@webpieces/http-routing": "0.4.796",
30
+ "@webpieces/http-server": "0.4.796",
31
31
  "express": "5.1.0",
32
32
  "reflect-metadata": "0.2.2",
33
33
  "tslib": "2.8.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@webpieces/http-client-node": "0.4.795"
36
+ "@webpieces/http-client-node": "0.4.796"
37
37
  }
38
38
  }
@@ -1,5 +1,5 @@
1
1
  import { CallToolResult } from '@modelcontextprotocol/server';
2
- import { ContextKey } from '@webpieces/core-util';
2
+ import { ContextKey, EndpointOperation } from '@webpieces/core-util';
3
3
  /** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
4
4
  export declare const MCP_REQUEST_ID_META_KEY = "webpieces/requestId";
5
5
  /** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */
@@ -20,8 +20,9 @@ export declare class McpErrorData {
20
20
  export declare class McpCorrelation {
21
21
  readonly jsonRpcId: string | number | null;
22
22
  readonly toolName?: string | undefined;
23
+ readonly operation?: EndpointOperation | undefined;
23
24
  static readonly KEY: ContextKey<McpCorrelation, "untrusted">;
24
- constructor(jsonRpcId: string | number | null, toolName?: string | undefined);
25
+ constructor(jsonRpcId: string | number | null, toolName?: string | undefined, operation?: EndpointOperation | undefined);
25
26
  /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
26
27
  static stamp(correlation: McpCorrelation): void;
27
28
  /** A renderer reached before anything stamped (a failure with no parseable body) still renders. */
@@ -32,6 +33,8 @@ export declare class McpCorrelation {
32
33
  /** The only error shape rendered into model-visible MCP tool content. */
33
34
  export declare class ModelVisibleToolError {
34
35
  readonly kind: string;
36
+ readonly category: 'bug' | 'caller' | 'dependency' | 'temporary' | 'access';
37
+ readonly retry: 'never' | 'after-correction' | 'after-delay' | 'safe' | 'unsafe-outcome-unknown';
35
38
  readonly message: string;
36
39
  readonly requestId: string;
37
40
  readonly field?: string | undefined;
@@ -39,7 +42,12 @@ export declare class ModelVisibleToolError {
39
42
  readonly errorCode?: string | undefined;
40
43
  readonly retryAfterSeconds?: number | undefined;
41
44
  readonly statusCode?: number | undefined;
42
- constructor(kind: string, message: string, requestId: string, field?: string | undefined, callerMessage?: string | undefined, errorCode?: string | undefined, retryAfterSeconds?: number | undefined, statusCode?: number | undefined);
45
+ constructor(kind: string, category: 'bug' | 'caller' | 'dependency' | 'temporary' | 'access', retry: 'never' | 'after-correction' | 'after-delay' | 'safe' | 'unsafe-outcome-unknown', message: string, requestId: string, field?: string | undefined, callerMessage?: string | undefined, errorCode?: string | undefined, retryAfterSeconds?: number | undefined, statusCode?: number | undefined);
46
+ }
47
+ /** Explicit content-level envelope for clients that hide CallToolResult.isError. */
48
+ export declare class ModelVisibleToolErrorEnvelope {
49
+ readonly error: ModelVisibleToolError;
50
+ constructor(error: ModelVisibleToolError);
43
51
  }
44
52
  /** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
45
53
  export declare class McpHttpErrorBody {
@@ -108,4 +116,9 @@ export declare class McpDefaultToolCallRenderer implements McpErrorTranslator {
108
116
  private readonly boundary;
109
117
  toWire(error: Error): CallToolResult;
110
118
  private modelVisible;
119
+ private modelVisibleRuntime;
120
+ private coded;
121
+ private bug;
122
+ private transient;
123
+ private visible;
111
124
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.McpDefaultToolCallRenderer = exports.McpHttpErrorDetail = exports.McpHttpErrorBody = exports.ModelVisibleToolError = exports.McpCorrelation = exports.McpErrorData = exports.MCP_REQUEST_ID_META_KEY = void 0;
3
+ exports.McpDefaultToolCallRenderer = exports.McpHttpErrorDetail = exports.McpHttpErrorBody = exports.ModelVisibleToolErrorEnvelope = exports.ModelVisibleToolError = exports.McpCorrelation = exports.McpErrorData = exports.MCP_REQUEST_ID_META_KEY = void 0;
4
4
  const core_util_1 = require("@webpieces/core-util");
5
5
  const core_context_1 = require("@webpieces/core-context");
6
6
  /** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
@@ -26,12 +26,14 @@ exports.McpErrorData = McpErrorData;
26
26
  class McpCorrelation {
27
27
  jsonRpcId;
28
28
  toolName;
29
+ operation;
29
30
  // webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only
30
31
  static KEY = core_util_1.ContextKey.untrusted('webpieces-mcp-correlation', undefined, false,
31
32
  /*isLogged*/ false);
32
- constructor(jsonRpcId, toolName) {
33
+ constructor(jsonRpcId, toolName, operation) {
33
34
  this.jsonRpcId = jsonRpcId;
34
35
  this.toolName = toolName;
36
+ this.operation = operation;
35
37
  }
36
38
  /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
37
39
  // webpieces-disable no-function-outside-class -- static accessor trio for one context key
@@ -53,6 +55,8 @@ exports.McpCorrelation = McpCorrelation;
53
55
  /** The only error shape rendered into model-visible MCP tool content. */
54
56
  class ModelVisibleToolError {
55
57
  kind;
58
+ category;
59
+ retry;
56
60
  message;
57
61
  requestId;
58
62
  field;
@@ -60,8 +64,10 @@ class ModelVisibleToolError {
60
64
  errorCode;
61
65
  retryAfterSeconds;
62
66
  statusCode;
63
- constructor(kind, message, requestId, field, callerMessage, errorCode, retryAfterSeconds, statusCode) {
67
+ constructor(kind, category, retry, message, requestId, field, callerMessage, errorCode, retryAfterSeconds, statusCode) {
64
68
  this.kind = kind;
69
+ this.category = category;
70
+ this.retry = retry;
65
71
  this.message = message;
66
72
  this.requestId = requestId;
67
73
  this.field = field;
@@ -72,6 +78,14 @@ class ModelVisibleToolError {
72
78
  }
73
79
  }
74
80
  exports.ModelVisibleToolError = ModelVisibleToolError;
81
+ /** Explicit content-level envelope for clients that hide CallToolResult.isError. */
82
+ class ModelVisibleToolErrorEnvelope {
83
+ error;
84
+ constructor(error) {
85
+ this.error = error;
86
+ }
87
+ }
88
+ exports.ModelVisibleToolErrorEnvelope = ModelVisibleToolErrorEnvelope;
75
89
  /** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
76
90
  class McpHttpErrorBody {
77
91
  error;
@@ -106,7 +120,7 @@ class McpDefaultToolCallRenderer {
106
120
  boundary = new core_util_1.ApiErrorBoundary();
107
121
  toWire(error) {
108
122
  const requestId = McpCorrelation.requestId();
109
- const visible = this.modelVisible(this.boundary.encode(error), requestId);
123
+ const visible = new ModelVisibleToolErrorEnvelope(this.modelVisible(this.boundary.encode(error), requestId));
110
124
  const text = JSON.stringify(visible);
111
125
  return {
112
126
  content: [{ type: 'text', text }],
@@ -115,21 +129,77 @@ class McpDefaultToolCallRenderer {
115
129
  };
116
130
  }
117
131
  modelVisible(payload, requestId) {
132
+ const support = `Give requestId ${requestId} to support so the internal failure can be located and fixed.`;
118
133
  switch (payload.kind) {
119
134
  case 'end-user':
120
- return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode);
135
+ return this.visible(payload, requestId, 'caller', 'after-correction', payload.message);
121
136
  case 'bad-request':
122
- return new ModelVisibleToolError(payload.kind, payload.callerMessage ?? payload.message, requestId, payload.field, payload.callerMessage);
137
+ return this.visible(payload, requestId, 'caller', 'after-correction', payload.callerMessage ?? payload.message);
138
+ case 'unauthorized':
139
+ case 'forbidden':
140
+ return this.visible(payload, requestId, 'access', 'after-correction', 'Access was denied. Retrying unchanged credentials and arguments will not help; correct the credentials or permissions first.');
141
+ case 'not-found':
142
+ case 'endpoint-not-found':
143
+ case 'conflict':
144
+ case 'unprocessable':
145
+ case 'precondition-failed':
146
+ case 'unsupported-media-type':
147
+ return this.visible(payload, requestId, 'caller', 'after-correction', `${payload.message}. Correct the request or current resource state before trying again.`);
148
+ case 'not-implemented':
149
+ return this.visible(payload, requestId, 'bug', 'never', `The tool does not implement this operation. Retrying or changing arguments will not help. ${support}`);
150
+ case 'dependency':
151
+ return this.visible(payload, requestId, 'dependency', 'never', `A downstream dependency failed. Retrying or changing arguments will not help. If the failure persists, ${support}`);
152
+ default:
153
+ return this.modelVisibleRuntime(payload, requestId, support);
154
+ }
155
+ }
156
+ modelVisibleRuntime(payload, requestId, support) {
157
+ switch (payload.kind) {
158
+ case 'request-timeout':
159
+ case 'dependency-timeout':
160
+ return this.transient(payload, requestId, 'The operation timed out; the previous operation may already have completed.');
161
+ case 'bad-gateway':
162
+ return this.transient(payload, requestId, 'A gateway returned an invalid upstream response; the previous operation may already have completed.');
163
+ case 'unavailable':
164
+ return this.transient(payload, requestId, 'A required service is temporarily unavailable; the previous operation may already have completed.');
165
+ case 'rate-limited':
166
+ case 'dependency-backoff':
167
+ return this.transient(payload, requestId, payload.retryAfterSeconds === undefined
168
+ ? 'The operation was throttled. Do not retry immediately.'
169
+ : `The operation was throttled. Wait at least ${payload.retryAfterSeconds} seconds before retrying.`);
123
170
  case 'coded':
124
- return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode, undefined, payload.statusCode);
171
+ return this.coded(payload, requestId, support);
125
172
  case 'implementation':
126
- return new ModelVisibleToolError(payload.kind, `Internal error in tool ${McpCorrelation.current().toolName ?? 'unknown'} ` +
127
- `(requestId ${requestId}). This is a bug in the tool, not in your ` +
128
- 'arguments; retrying with different arguments will not help.', requestId);
173
+ case 'connection':
174
+ return this.bug(payload, requestId, support);
129
175
  default:
130
- return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, undefined, payload.retryAfterSeconds);
176
+ return this.bug(payload, requestId, support);
131
177
  }
132
178
  }
179
+ coded(payload, requestId, support) {
180
+ if (payload.statusCode === 408 || payload.statusCode === 429) {
181
+ return this.transient(payload, requestId, `The operation failed with HTTP ${payload.statusCode}; the previous operation's outcome may be unknown.`);
182
+ }
183
+ if ((payload.statusCode ?? 500) >= 500) {
184
+ return this.visible(payload, requestId, 'dependency', 'never', `A downstream service failed with HTTP ${payload.statusCode ?? 500}. Retrying or changing arguments will not help. ${support}`);
185
+ }
186
+ return this.visible(payload, requestId, 'caller', 'after-correction', payload.message);
187
+ }
188
+ bug(payload, requestId, support) {
189
+ return this.visible(payload, requestId, 'bug', 'never', `The tool ${McpCorrelation.current().toolName ?? 'unknown'} encountered an internal bug. ` +
190
+ `Retrying or changing the arguments will not help. ${support}`);
191
+ }
192
+ transient(payload, requestId, message) {
193
+ const operation = McpCorrelation.current().operation ?? 'write';
194
+ if (operation === 'write') {
195
+ return this.visible(payload, requestId, 'temporary', 'unsafe-outcome-unknown', `${message} This endpoint is a non-idempotent write, so retrying could duplicate it; verify the outcome before retrying.`);
196
+ }
197
+ const retry = payload.retryAfterSeconds === undefined ? 'safe' : 'after-delay';
198
+ return this.visible(payload, requestId, 'temporary', retry, `${message} This endpoint is ${operation === 'read' ? 'read-only' : 'idempotent'}, so retrying is safe${payload.retryAfterSeconds === undefined ? ' after a delay' : ` after ${payload.retryAfterSeconds} seconds`}.`);
199
+ }
200
+ visible(payload, requestId, category, retry, message) {
201
+ return new ModelVisibleToolError(payload.kind, category, retry, message, requestId, payload.field, payload.callerMessage, payload.errorCode, payload.retryAfterSeconds, payload.statusCode);
202
+ }
133
203
  }
134
204
  exports.McpDefaultToolCallRenderer = McpDefaultToolCallRenderer;
135
205
  //# sourceMappingURL=McpToolCallRendering.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"McpToolCallRendering.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolCallRendering.ts"],"names":[],"mappings":";;;AACA,oDAM8B;AAC9B,0DAAyD;AAEzD,mGAAmG;AACtF,QAAA,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,6FAA6F;AAC7F,MAAa,YAAY;IACO;IAA5B,YAA4B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CACpD;AAFD,oCAEC;AAED;;;;;;;;;GASG;AACH,MAAa,cAAc;IAUH;IACA;IAVpB,mGAAmG;IACnG,MAAM,CAAU,GAAG,GAAG,sBAAU,CAAC,SAAS,CACtC,2BAA2B,EAC3B,SAAS,EACT,KAAK;IACL,YAAY,CAAC,KAAK,CACrB,CAAC;IAEF,YACoB,SAAiC,EACjC,QAAiB;QADjB,cAAS,GAAT,SAAS,CAAwB;QACjC,aAAQ,GAAR,QAAQ,CAAS;IAClC,CAAC;IAEJ,iGAAiG;IACjG,0FAA0F;IAC1F,MAAM,CAAC,KAAK,CAAC,WAA2B;QACpC,6BAAc,CAAC,YAAY,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACjE,CAAC;IAED,mGAAmG;IACnG,0FAA0F;IAC1F,MAAM,CAAC,OAAO;QACV,OAAO,6BAAc,CAAC,YAAY,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACvF,CAAC;IAED,0FAA0F;IAC1F,0FAA0F;IAC1F,MAAM,CAAC,SAAS;QACZ,OAAO,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,IAAI,oBAAoB,CAAC;IAChG,CAAC;;AA9BL,wCA+BC;AAED,yEAAyE;AACzE,MAAa,qBAAqB;IAEV;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IARpB,YACoB,IAAY,EACZ,OAAe,EACf,SAAiB,EACjB,KAAc,EACd,aAAsB,EACtB,SAAkB,EAClB,iBAA0B,EAC1B,UAAmB;QAPnB,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,cAAS,GAAT,SAAS,CAAQ;QACjB,UAAK,GAAL,KAAK,CAAS;QACd,kBAAa,GAAb,aAAa,CAAS;QACtB,cAAS,GAAT,SAAS,CAAS;QAClB,sBAAiB,GAAjB,iBAAiB,CAAS;QAC1B,eAAU,GAAV,UAAU,CAAS;IACpC,CAAC;CACP;AAXD,sDAWC;AAED,mFAAmF;AACnF,MAAa,gBAAgB;IAGL;IACA;IAHX,OAAO,GAAG,KAAK,CAAC;IACzB,YACoB,KAAyB,EACzB,EAA0B;QAD1B,UAAK,GAAL,KAAK,CAAoB;QACzB,OAAE,GAAF,EAAE,CAAwB;IAC3C,CAAC;CACP;AAND,4CAMC;AAED,MAAa,kBAAkB;IAEP;IACA;IACA;IAHpB,YACoB,IAAY,EACZ,OAAe,EACf,IAAkB;QAFlB,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAAc;IACnC,CAAC;CACP;AAND,gDAMC;AA6CD;;;;;;;GAOG;AACH,MAAa,0BAA0B;IAClB,QAAQ,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAEnD,MAAM,CAAC,KAAY;QACf,MAAM,SAAS,GAAG,cAAc,CAAC,SAAS,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;QAC1E,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAmB,CAAC,CAAC;QACjD,OAAO;YACH,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,EAAE,CAAC,+BAAuB,CAAC,EAAE,SAAS,EAAE;SAClD,CAAC;IACN,CAAC;IAEO,YAAY,CAAC,OAAwB,EAAE,SAAiB;QAC5D,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,UAAU;gBACX,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,CAAC,SAAS,CACpB,CAAC;YACN,KAAK,aAAa;gBACd,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,OAAO,EACxC,SAAS,EACT,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,aAAa,CACxB,CAAC;YACN,KAAK,OAAO;gBACR,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,CAAC,SAAS,EACjB,SAAS,EACT,OAAO,CAAC,UAAU,CACrB,CAAC;YACN,KAAK,gBAAgB;gBACjB,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,0BAA0B,cAAc,CAAC,OAAO,EAAE,CAAC,QAAQ,IAAI,SAAS,GAAG;oBACvE,cAAc,SAAS,4CAA4C;oBACnE,6DAA6D,EACjE,SAAS,CACZ,CAAC;YACN;gBACI,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,CAAC,iBAAiB,CAC5B,CAAC;QACV,CAAC;IACL,CAAC;CACJ;AAhED,gEAgEC","sourcesContent":["import { CallToolResult } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n ApiErrorPayload,\n ContextKey,\n DtoValue,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/**\n * The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,\n * and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than\n * threaded through every renderer signature, because `requestId` already travels that way\n * ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY\n * of the context — which is what the deleted `McpFailureScope` was.\n *\n * Context-only and never logged: the logging backends read `RequestContext` for correlation on every\n * record already, and this object exists purely so the renderers can be one-argument functions.\n */\nexport class McpCorrelation {\n // webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only\n static readonly KEY = ContextKey.untrusted<McpCorrelation>(\n 'webpieces-mcp-correlation',\n undefined,\n false,\n /*isLogged*/ false,\n );\n\n constructor(\n public readonly jsonRpcId: string | number | null,\n public readonly toolName?: string,\n ) {}\n\n /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static stamp(correlation: McpCorrelation): void {\n RequestContext.putUntrusted(McpCorrelation.KEY, correlation);\n }\n\n /** A renderer reached before anything stamped (a failure with no parseable body) still renders. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static current(): McpCorrelation {\n return RequestContext.getUntrusted(McpCorrelation.KEY) ?? new McpCorrelation(null);\n }\n\n /** The webpieces requestId of the request being served, as every MCP reply reports it. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static requestId(): string {\n return RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';\n }\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslator` an app\n * registers on `ClientRegistry` for HTTP and the `IpcErrorTranslator` it registers on `IpcRegistry`.\n * Register one with {@link McpRegistry.setErrorTranslator}.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * # There is NO `fromWire`, and that is the protocol, not an oversight\n *\n * The HTTP and IPC translators have two halves because webpieces sits on BOTH ends of those wires: it\n * writes the response AND, in another process, reads one. **webpieces is never the MCP client.** The\n * client is Claude (or another model host); there is no return path for webpieces to translate, so\n * the pair is one-directional by nature. Do not \"restore the symmetry\" by adding a half with nothing\n * to call it.\n *\n * # The pre-SDK boundary does NOT share the HTTP `ErrorTranslator`, despite the identical type\n *\n * `WpMcpErrorTranslator.toBearerBoundaryResponse` returns an `HttpResponseDto`, exactly like\n * `ErrorTranslator.toWire` — and it must STILL not be routed through it. Two reasons, both hard:\n *\n * - its body must stay JSON-RPC shaped (`McpHttpErrorBody`) or an MCP client cannot parse the reply\n * at all, whereas an app's HTTP translator publishes the app's own REST envelope;\n * - it emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` that MCP clients use for OAuth\n * discovery, so an app rewriting it breaks connector onboarding.\n *\n * Same TYPE, different OWNER. `tools/list` is framework-owned for the same reason.\n *\n * Contract:\n * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.\n * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one\n * error value on this path.\n * - There is no \"not mine\" return. A registered translator REPLACES the webpieces default and\n * declines by DELEGATING to it: `new McpDefaultToolCallRenderer().toWire(error)`.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - scope is `tools/call` ONLY.\n */\nexport interface McpErrorTranslator {\n /** An error in, the whole `tools/call` reply out. Named for its HTTP and IPC twins. */\n toWire(error: Error): CallToolResult;\n}\n\n/**\n * webpieces' DEFAULT `tools/call` rendering, and what {@link McpRegistry} holds until an app installs\n * its own. Public so an app's own {@link McpErrorTranslator} can DECLINE an error by delegating to it\n * — the MCP twin of `WebpiecesDefaultErrorTranslator.toWire` on the HTTP side.\n *\n * Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own\n * message in front of a model unless it is an `ApiEndUserError`.\n */\nexport class McpDefaultToolCallRenderer implements McpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n toWire(error: Error): CallToolResult {\n const requestId = McpCorrelation.requestId();\n const visible = this.modelVisible(this.boundary.encode(error), requestId);\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: { [MCP_REQUEST_ID_META_KEY]: requestId },\n };\n }\n\n private modelVisible(payload: ApiErrorPayload, requestId: string): ModelVisibleToolError {\n switch (payload.kind) {\n case 'end-user':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n payload.errorCode,\n );\n case 'bad-request':\n return new ModelVisibleToolError(\n payload.kind,\n payload.callerMessage ?? payload.message,\n requestId,\n payload.field,\n payload.callerMessage,\n );\n case 'coded':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n payload.errorCode,\n undefined,\n payload.statusCode,\n );\n case 'implementation':\n return new ModelVisibleToolError(\n payload.kind,\n `Internal error in tool ${McpCorrelation.current().toolName ?? 'unknown'} ` +\n `(requestId ${requestId}). This is a bug in the tool, not in your ` +\n 'arguments; retrying with different arguments will not help.',\n requestId,\n );\n default:\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n undefined,\n payload.retryAfterSeconds,\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"McpToolCallRendering.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolCallRendering.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n ApiErrorPayload,\n ContextKey,\n DtoValue,\n EndpointOperation,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/**\n * The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,\n * and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than\n * threaded through every renderer signature, because `requestId` already travels that way\n * ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY\n * of the context — which is what the deleted `McpFailureScope` was.\n *\n * Context-only and never logged: the logging backends read `RequestContext` for correlation on every\n * record already, and this object exists purely so the renderers can be one-argument functions.\n */\nexport class McpCorrelation {\n // webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only\n static readonly KEY = ContextKey.untrusted<McpCorrelation>(\n 'webpieces-mcp-correlation',\n undefined,\n false,\n /*isLogged*/ false,\n );\n\n constructor(\n public readonly jsonRpcId: string | number | null,\n public readonly toolName?: string,\n public readonly operation?: EndpointOperation,\n ) {}\n\n /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static stamp(correlation: McpCorrelation): void {\n RequestContext.putUntrusted(McpCorrelation.KEY, correlation);\n }\n\n /** A renderer reached before anything stamped (a failure with no parseable body) still renders. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static current(): McpCorrelation {\n return RequestContext.getUntrusted(McpCorrelation.KEY) ?? new McpCorrelation(null);\n }\n\n /** The webpieces requestId of the request being served, as every MCP reply reports it. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static requestId(): string {\n return RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';\n }\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly category: 'bug' | 'caller' | 'dependency' | 'temporary' | 'access',\n public readonly retry:\n | 'never'\n | 'after-correction'\n | 'after-delay'\n | 'safe'\n | 'unsafe-outcome-unknown',\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** Explicit content-level envelope for clients that hide CallToolResult.isError. */\nexport class ModelVisibleToolErrorEnvelope {\n constructor(public readonly error: ModelVisibleToolError) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslator` an app\n * registers on `ClientRegistry` for HTTP and the `IpcErrorTranslator` it registers on `IpcRegistry`.\n * Register one with {@link McpRegistry.setErrorTranslator}.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * # There is NO `fromWire`, and that is the protocol, not an oversight\n *\n * The HTTP and IPC translators have two halves because webpieces sits on BOTH ends of those wires: it\n * writes the response AND, in another process, reads one. **webpieces is never the MCP client.** The\n * client is Claude (or another model host); there is no return path for webpieces to translate, so\n * the pair is one-directional by nature. Do not \"restore the symmetry\" by adding a half with nothing\n * to call it.\n *\n * # The pre-SDK boundary does NOT share the HTTP `ErrorTranslator`, despite the identical type\n *\n * `WpMcpErrorTranslator.toBearerBoundaryResponse` returns an `HttpResponseDto`, exactly like\n * `ErrorTranslator.toWire` — and it must STILL not be routed through it. Two reasons, both hard:\n *\n * - its body must stay JSON-RPC shaped (`McpHttpErrorBody`) or an MCP client cannot parse the reply\n * at all, whereas an app's HTTP translator publishes the app's own REST envelope;\n * - it emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` that MCP clients use for OAuth\n * discovery, so an app rewriting it breaks connector onboarding.\n *\n * Same TYPE, different OWNER. `tools/list` is framework-owned for the same reason.\n *\n * Contract:\n * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.\n * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one\n * error value on this path.\n * - There is no \"not mine\" return. A registered translator REPLACES the webpieces default and\n * declines by DELEGATING to it: `new McpDefaultToolCallRenderer().toWire(error)`.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - scope is `tools/call` ONLY.\n */\nexport interface McpErrorTranslator {\n /** An error in, the whole `tools/call` reply out. Named for its HTTP and IPC twins. */\n toWire(error: Error): CallToolResult;\n}\n\n/**\n * webpieces' DEFAULT `tools/call` rendering, and what {@link McpRegistry} holds until an app installs\n * its own. Public so an app's own {@link McpErrorTranslator} can DECLINE an error by delegating to it\n * — the MCP twin of `WebpiecesDefaultErrorTranslator.toWire` on the HTTP side.\n *\n * Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own\n * message in front of a model unless it is an `ApiEndUserError`.\n */\nexport class McpDefaultToolCallRenderer implements McpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n toWire(error: Error): CallToolResult {\n const requestId = McpCorrelation.requestId();\n const visible = new ModelVisibleToolErrorEnvelope(\n this.modelVisible(this.boundary.encode(error), requestId),\n );\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: { [MCP_REQUEST_ID_META_KEY]: requestId },\n };\n }\n\n private modelVisible(payload: ApiErrorPayload, requestId: string): ModelVisibleToolError {\n const support = `Give requestId ${requestId} to support so the internal failure can be located and fixed.`;\n switch (payload.kind) {\n case 'end-user':\n return this.visible(\n payload,\n requestId,\n 'caller',\n 'after-correction',\n payload.message,\n );\n case 'bad-request':\n return this.visible(\n payload,\n requestId,\n 'caller',\n 'after-correction',\n payload.callerMessage ?? payload.message,\n );\n case 'unauthorized':\n case 'forbidden':\n return this.visible(\n payload,\n requestId,\n 'access',\n 'after-correction',\n 'Access was denied. Retrying unchanged credentials and arguments will not help; correct the credentials or permissions first.',\n );\n case 'not-found':\n case 'endpoint-not-found':\n case 'conflict':\n case 'unprocessable':\n case 'precondition-failed':\n case 'unsupported-media-type':\n return this.visible(\n payload,\n requestId,\n 'caller',\n 'after-correction',\n `${payload.message}. Correct the request or current resource state before trying again.`,\n );\n case 'not-implemented':\n return this.visible(\n payload,\n requestId,\n 'bug',\n 'never',\n `The tool does not implement this operation. Retrying or changing arguments will not help. ${support}`,\n );\n case 'dependency':\n return this.visible(\n payload,\n requestId,\n 'dependency',\n 'never',\n `A downstream dependency failed. Retrying or changing arguments will not help. If the failure persists, ${support}`,\n );\n default:\n return this.modelVisibleRuntime(payload, requestId, support);\n }\n }\n\n private modelVisibleRuntime(\n payload: ApiErrorPayload,\n requestId: string,\n support: string,\n ): ModelVisibleToolError {\n switch (payload.kind) {\n case 'request-timeout':\n case 'dependency-timeout':\n return this.transient(\n payload,\n requestId,\n 'The operation timed out; the previous operation may already have completed.',\n );\n case 'bad-gateway':\n return this.transient(\n payload,\n requestId,\n 'A gateway returned an invalid upstream response; the previous operation may already have completed.',\n );\n case 'unavailable':\n return this.transient(\n payload,\n requestId,\n 'A required service is temporarily unavailable; the previous operation may already have completed.',\n );\n case 'rate-limited':\n case 'dependency-backoff':\n return this.transient(\n payload,\n requestId,\n payload.retryAfterSeconds === undefined\n ? 'The operation was throttled. Do not retry immediately.'\n : `The operation was throttled. Wait at least ${payload.retryAfterSeconds} seconds before retrying.`,\n );\n case 'coded':\n return this.coded(payload, requestId, support);\n case 'implementation':\n case 'connection':\n return this.bug(payload, requestId, support);\n default:\n return this.bug(payload, requestId, support);\n }\n }\n\n private coded(\n payload: ApiErrorPayload,\n requestId: string,\n support: string,\n ): ModelVisibleToolError {\n if (payload.statusCode === 408 || payload.statusCode === 429) {\n return this.transient(\n payload,\n requestId,\n `The operation failed with HTTP ${payload.statusCode}; the previous operation's outcome may be unknown.`,\n );\n }\n if ((payload.statusCode ?? 500) >= 500) {\n return this.visible(\n payload,\n requestId,\n 'dependency',\n 'never',\n `A downstream service failed with HTTP ${payload.statusCode ?? 500}. Retrying or changing arguments will not help. ${support}`,\n );\n }\n return this.visible(payload, requestId, 'caller', 'after-correction', payload.message);\n }\n\n private bug(\n payload: ApiErrorPayload,\n requestId: string,\n support: string,\n ): ModelVisibleToolError {\n return this.visible(\n payload,\n requestId,\n 'bug',\n 'never',\n `The tool ${McpCorrelation.current().toolName ?? 'unknown'} encountered an internal bug. ` +\n `Retrying or changing the arguments will not help. ${support}`,\n );\n }\n\n private transient(\n payload: ApiErrorPayload,\n requestId: string,\n message: string,\n ): ModelVisibleToolError {\n const operation = McpCorrelation.current().operation ?? 'write';\n if (operation === 'write') {\n return this.visible(\n payload,\n requestId,\n 'temporary',\n 'unsafe-outcome-unknown',\n `${message} This endpoint is a non-idempotent write, so retrying could duplicate it; verify the outcome before retrying.`,\n );\n }\n const retry = payload.retryAfterSeconds === undefined ? 'safe' : 'after-delay';\n return this.visible(\n payload,\n requestId,\n 'temporary',\n retry,\n `${message} This endpoint is ${operation === 'read' ? 'read-only' : 'idempotent'}, so retrying is safe${payload.retryAfterSeconds === undefined ? ' after a delay' : ` after ${payload.retryAfterSeconds} seconds`}.`,\n );\n }\n\n private visible(\n payload: ApiErrorPayload,\n requestId: string,\n category: ModelVisibleToolError['category'],\n retry: ModelVisibleToolError['retry'],\n message: string,\n ): ModelVisibleToolError {\n return new ModelVisibleToolError(\n payload.kind,\n category,\n retry,\n message,\n requestId,\n payload.field,\n payload.callerMessage,\n payload.errorCode,\n payload.retryAfterSeconds,\n payload.statusCode,\n );\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { ApiJsonSchema, AuthMeta, DtoClass, DtoSchemaBuilder, WpMcpJwtAuthMetadata, WpMcpToolMetadata } from '@webpieces/core-util';
1
+ import { ApiJsonSchema, AuthMeta, DtoClass, DtoSchemaBuilder, EndpointOperation, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
2
2
  import { ClassType } from '@webpieces/http-routing';
3
3
  import { McpApiBinding } from './McpApiBinding';
4
4
  /** Fully resolved contract metadata for one MCP tool. */
@@ -7,7 +7,8 @@ export declare class RegisteredMcpTool {
7
7
  readonly methodName: string;
8
8
  readonly name: string;
9
9
  readonly description: string;
10
- readonly annotations: WpMcpToolMetadata['hints'];
10
+ readonly annotations: WpMcpToolHints;
11
+ readonly operation: EndpointOperation;
11
12
  readonly authMeta: AuthMeta;
12
13
  readonly mcpAuth: WpMcpJwtAuthMetadata;
13
14
  readonly binding: McpApiBinding;
@@ -15,7 +16,7 @@ export declare class RegisteredMcpTool {
15
16
  readonly responseClass: DtoClass;
16
17
  readonly inputSchema: ApiJsonSchema;
17
18
  readonly outputSchema: ApiJsonSchema;
18
- constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolMetadata['hints'], authMeta: AuthMeta, mcpAuth: WpMcpJwtAuthMetadata, binding: McpApiBinding, requestClass: DtoClass, responseClass: DtoClass, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
19
+ constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolHints, operation: EndpointOperation, authMeta: AuthMeta, mcpAuth: WpMcpJwtAuthMetadata, binding: McpApiBinding, requestClass: DtoClass, responseClass: DtoClass, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
19
20
  /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */
20
21
  isVisibleTo(listingRoles: readonly string[]): boolean;
21
22
  }
@@ -9,6 +9,7 @@ class RegisteredMcpTool {
9
9
  name;
10
10
  description;
11
11
  annotations;
12
+ operation;
12
13
  authMeta;
13
14
  mcpAuth;
14
15
  binding;
@@ -16,12 +17,13 @@ class RegisteredMcpTool {
16
17
  responseClass;
17
18
  inputSchema;
18
19
  outputSchema;
19
- constructor(apiClass, methodName, name, description, annotations, authMeta, mcpAuth, binding, requestClass, responseClass, inputSchema, outputSchema) {
20
+ constructor(apiClass, methodName, name, description, annotations, operation, authMeta, mcpAuth, binding, requestClass, responseClass, inputSchema, outputSchema) {
20
21
  this.apiClass = apiClass;
21
22
  this.methodName = methodName;
22
23
  this.name = name;
23
24
  this.description = description;
24
25
  this.annotations = annotations;
26
+ this.operation = operation;
25
27
  this.authMeta = authMeta;
26
28
  this.mcpAuth = mcpAuth;
27
29
  this.binding = binding;
@@ -77,7 +79,8 @@ class McpToolRegistry {
77
79
  binding.validateMethod(metadata.methodName);
78
80
  const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);
79
81
  const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);
80
- return new RegisteredMcpTool(apiClass, metadata.methodName, metadata.name, metadata.description, metadata.hints, authMeta, mcpAuth, binding, requestClass, responseClass, this.schemaBuilder.build(requestClass), this.schemaBuilder.build(responseClass));
82
+ const operation = (0, core_util_1.getEndpointOperation)(apiClass, metadata.methodName);
83
+ return new RegisteredMcpTool(apiClass, metadata.methodName, metadata.name, metadata.description, (0, core_util_1.mcpHintsForOperation)(operation, metadata.openWorldHint), operation, authMeta, mcpAuth, binding, requestClass, responseClass, this.schemaBuilder.build(requestClass), this.schemaBuilder.build(responseClass));
81
84
  }
82
85
  }
83
86
  exports.McpToolRegistry = McpToolRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n AuthMeta,\n DtoClass,\n DtoSchemaBuilder,\n getAuthMeta,\n getEndpointKind,\n getEndpoints,\n getWpMcpTools,\n getWpMcpAuthJwt,\n rolesRequired,\n WpMcpJwtAuthMetadata,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\n\n/** Fully resolved contract metadata for one MCP tool. */\nexport class RegisteredMcpTool {\n constructor(\n public readonly apiClass: ClassType,\n public readonly methodName: string,\n public readonly name: string,\n public readonly description: string,\n public readonly annotations: WpMcpToolMetadata['hints'],\n public readonly authMeta: AuthMeta,\n public readonly mcpAuth: WpMcpJwtAuthMetadata,\n public readonly binding: McpApiBinding,\n public readonly requestClass: DtoClass,\n public readonly responseClass: DtoClass,\n public readonly inputSchema: ApiJsonSchema,\n public readonly outputSchema: ApiJsonSchema,\n ) {}\n\n /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */\n isVisibleTo(listingRoles: readonly string[]): boolean {\n const required = rolesRequired(this.mcpAuth.requirement);\n return (\n required.length === 0 || required.some((role: string) => listingRoles.includes(role))\n );\n }\n}\n\n/** Fail-fast registry built entirely from API/decorator metadata. */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(bindings: readonly McpApiBinding[]) {\n const registered: RegisteredMcpTool[] = [];\n const names = new Set<string>();\n for (const binding of bindings) {\n const apiClass = binding.api;\n for (const metadata of getWpMcpTools(apiClass)) {\n if (names.has(metadata.name)) {\n throw new Error(\n `Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`,\n );\n }\n names.add(metadata.name);\n registered.push(this.resolve(apiClass, metadata, binding));\n }\n }\n this.tools = registered;\n }\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n ): RegisteredMcpTool {\n const endpoints = getEndpoints(apiClass) ?? {};\n if (!endpoints[metadata.methodName]) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`,\n );\n }\n if (getEndpointKind(apiClass, metadata.methodName) !== 'rpc') {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`,\n );\n }\n const authMeta = getAuthMeta(apiClass, metadata.methodName);\n if (!authMeta)\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} has no HTTP auth.`);\n const mcpAuth = getWpMcpAuthJwt(apiClass, metadata.methodName);\n if (!mcpAuth) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must declare @WpMcpAuthJwt(...).`,\n );\n }\n binding.validateMethod(metadata.methodName);\n const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);\n const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);\n return new RegisteredMcpTool(\n apiClass,\n metadata.methodName,\n metadata.name,\n metadata.description,\n metadata.hints,\n authMeta,\n mcpAuth,\n binding,\n requestClass,\n responseClass,\n this.schemaBuilder.build(requestClass),\n this.schemaBuilder.build(responseClass),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n AuthMeta,\n DtoClass,\n DtoSchemaBuilder,\n getAuthMeta,\n getEndpointKind,\n getEndpointOperation,\n getEndpoints,\n getWpMcpTools,\n getWpMcpAuthJwt,\n rolesRequired,\n mcpHintsForOperation,\n EndpointOperation,\n WpMcpJwtAuthMetadata,\n WpMcpToolHints,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\n\n/** Fully resolved contract metadata for one MCP tool. */\nexport class RegisteredMcpTool {\n constructor(\n public readonly apiClass: ClassType,\n public readonly methodName: string,\n public readonly name: string,\n public readonly description: string,\n public readonly annotations: WpMcpToolHints,\n public readonly operation: EndpointOperation,\n public readonly authMeta: AuthMeta,\n public readonly mcpAuth: WpMcpJwtAuthMetadata,\n public readonly binding: McpApiBinding,\n public readonly requestClass: DtoClass,\n public readonly responseClass: DtoClass,\n public readonly inputSchema: ApiJsonSchema,\n public readonly outputSchema: ApiJsonSchema,\n ) {}\n\n /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */\n isVisibleTo(listingRoles: readonly string[]): boolean {\n const required = rolesRequired(this.mcpAuth.requirement);\n return (\n required.length === 0 || required.some((role: string) => listingRoles.includes(role))\n );\n }\n}\n\n/** Fail-fast registry built entirely from API/decorator metadata. */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(bindings: readonly McpApiBinding[]) {\n const registered: RegisteredMcpTool[] = [];\n const names = new Set<string>();\n for (const binding of bindings) {\n const apiClass = binding.api;\n for (const metadata of getWpMcpTools(apiClass)) {\n if (names.has(metadata.name)) {\n throw new Error(\n `Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`,\n );\n }\n names.add(metadata.name);\n registered.push(this.resolve(apiClass, metadata, binding));\n }\n }\n this.tools = registered;\n }\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n ): RegisteredMcpTool {\n const endpoints = getEndpoints(apiClass) ?? {};\n if (!endpoints[metadata.methodName]) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`,\n );\n }\n if (getEndpointKind(apiClass, metadata.methodName) !== 'rpc') {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`,\n );\n }\n const authMeta = getAuthMeta(apiClass, metadata.methodName);\n if (!authMeta)\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} has no HTTP auth.`);\n const mcpAuth = getWpMcpAuthJwt(apiClass, metadata.methodName);\n if (!mcpAuth) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must declare @WpMcpAuthJwt(...).`,\n );\n }\n binding.validateMethod(metadata.methodName);\n const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);\n const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);\n const operation = getEndpointOperation(apiClass, metadata.methodName);\n return new RegisteredMcpTool(\n apiClass,\n metadata.methodName,\n metadata.name,\n metadata.description,\n mcpHintsForOperation(operation, metadata.openWorldHint),\n operation,\n authMeta,\n mcpAuth,\n binding,\n requestClass,\n responseClass,\n this.schemaBuilder.build(requestClass),\n this.schemaBuilder.build(responseClass),\n );\n }\n}\n"]}
@@ -324,6 +324,7 @@ class WpMcpServer {
324
324
  const tool = this.requireRegistry().find(name);
325
325
  if (!tool)
326
326
  throw this.translator.unknownTool(name);
327
+ McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, name, tool.operation));
327
328
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP tools/call entry point; delegates only to WpMcpErrorTranslator
328
329
  try {
329
330
  const result = await this.callTool(tool, request, authentication, sdkContext);