@ampless/mcp-server 1.0.0-beta.66 → 1.0.0-beta.68

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.ja.md CHANGED
@@ -71,13 +71,25 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
71
71
  ### `./jsonrpc`
72
72
 
73
73
  ```typescript
74
- import { dispatchJsonRpc } from '@ampless/mcp-server/jsonrpc'
75
- import type { JsonRpcRequest, JsonRpcResponse } from '@ampless/mcp-server/jsonrpc'
74
+ import {
75
+ dispatchJsonRpcMessage,
76
+ dispatchJsonRpc,
77
+ ToolUserError,
78
+ MAX_BATCH,
79
+ } from '@ampless/mcp-server/jsonrpc'
80
+ import type {
81
+ JsonRpcRequest,
82
+ JsonRpcResponse,
83
+ JsonRpcMessageResult,
84
+ } from '@ampless/mcp-server/jsonrpc'
76
85
  ```
77
86
 
78
87
  | エクスポート | 説明 |
79
88
  |---|---|
80
- | `dispatchJsonRpc(req, opts)` | JSON-RPC リクエストを 1 件ツールレジストリに対して実行(`initialize` のバージョンネゴシエーション、annotations 付き `tools/list`、`tools/call`、notification 処理)。notification(`id` 無し)は method を実行しつつレスポンスの代わりに `null` を返す。`id: null` / 小数 id は `INVALID_REQUEST` で拒否 |
89
+ | `dispatchJsonRpcMessage(input, opts)` | **推奨エントリポイント。** 未検証のデコード済みメッセージ(`unknown` 単一オブジェクト **または** batch 配列)を受け取り、タグ付き `JsonRpcMessageResult` を返す:`{ status: 'invalid', body }`(HTTP 400 — 非オブジェクト // 過大 batch)、`{ status: 'ok', body }`(HTTP 200 — 単一レスポンス、または batch のレスポンス配列)、`{ status: 'no-content' }`(HTTP 202 — notification のみ)。エンベロープ検証と batch 処理(逐次・順序維持・batch `initialize` 禁止)をここに集約したので、トランスポートは `JSON.parse` の生の結果をそのまま渡せる。`opts.maxBatch` の既定は `MAX_BATCH` |
90
+ | `dispatchJsonRpc(req, opts)` | **検証済みの** JSON-RPC リクエストを 1 件ツールレジストリに対して実行(`initialize` のバージョンネゴシエーション、annotations 付き `tools/list`、`tools/call`、notification 処理)。notification(`id` 無し)は method を実行しつつレスポンスの代わりに `null` を返す。`id: null` / 小数 id は `INVALID_REQUEST` で拒否。ワイヤから来たものはエンベロープ検証と batch 対応を集約した `dispatchJsonRpcMessage` を使うこと |
91
+ | `ToolUserError` / `isToolUserError` | client に返して安全な message を持つ想定内の tool failure を示す。独立 bundle の package entry 間でも認識でき、dispatcher はこの class に対して `formatToolError` と logging を迂回する。message に secret、内部詳細、未処理のユーザ入力を絶対に入れないこと |
92
+ | `MAX_BATCH` | batch 要素数の既定上限(`50`) |
81
93
  | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | エンベロープヘルパ + 標準エラーコード |
82
94
  | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
95
 
package/README.md CHANGED
@@ -71,13 +71,25 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
71
71
  ### `./jsonrpc`
72
72
 
73
73
  ```typescript
74
- import { dispatchJsonRpc } from '@ampless/mcp-server/jsonrpc'
75
- import type { JsonRpcRequest, JsonRpcResponse } from '@ampless/mcp-server/jsonrpc'
74
+ import {
75
+ dispatchJsonRpcMessage,
76
+ dispatchJsonRpc,
77
+ ToolUserError,
78
+ MAX_BATCH,
79
+ } from '@ampless/mcp-server/jsonrpc'
80
+ import type {
81
+ JsonRpcRequest,
82
+ JsonRpcResponse,
83
+ JsonRpcMessageResult,
84
+ } from '@ampless/mcp-server/jsonrpc'
76
85
  ```
77
86
 
78
87
  | Export | Description |
79
88
  |---|---|
80
- | `dispatchJsonRpc(req, opts)` | Runs one JSON-RPC request against a tool registry (`initialize` with protocol negotiation, `tools/list` with annotations, `tools/call`, notification handling). A notification (`id` absent) still executes the method but returns `null` instead of a response; `id: null` / fractional ids are rejected as `INVALID_REQUEST`. |
89
+ | `dispatchJsonRpcMessage(input, opts)` | **Preferred entry point.** Takes an *unvalidated* decoded message (`unknown` a single object **or** a batch array) and returns a tagged `JsonRpcMessageResult`: `{ status: 'invalid', body }` (HTTP 400 — top-level malformed / empty / over-size batch), `{ status: 'ok', body }` (HTTP 200 — single response or a batch's response array), or `{ status: 'no-content' }` (HTTP 202 — notification(s) only). Envelope validation and batch handling (sequential, order-preserving, `initialize` forbidden inside a batch) live here, so transports pass their raw `JSON.parse` result straight through. `opts.maxBatch` defaults to `MAX_BATCH`. |
90
+ | `dispatchJsonRpc(req, opts)` | Runs **one already-validated** JSON-RPC request against a tool registry (`initialize` with protocol negotiation, `tools/list` with annotations, `tools/call`, notification handling). A notification (`id` absent) still executes the method but returns `null` instead of a response; `id: null` / fractional ids are rejected as `INVALID_REQUEST`. Prefer `dispatchJsonRpcMessage` for anything coming off the wire — it centralises envelope checks and batch support. |
91
+ | `ToolUserError` / `isToolUserError` | Marks an expected tool failure whose message is safe to return to clients, including across independently bundled package entry points. The dispatcher bypasses `formatToolError` and logging for this class. Never put secrets, internal details, or unprocessed user input in its message. |
92
+ | `MAX_BATCH` | Default batch-element cap (`50`) |
81
93
  | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | Envelope helpers + standard error codes |
82
94
  | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
95
 
@@ -0,0 +1,211 @@
1
+ // src/jsonrpc/index.ts
2
+ var JSON_RPC_PARSE_ERROR = -32700;
3
+ var JSON_RPC_INVALID_REQUEST = -32600;
4
+ var JSON_RPC_METHOD_NOT_FOUND = -32601;
5
+ var JSON_RPC_INVALID_PARAMS = -32602;
6
+ var JSON_RPC_INTERNAL_ERROR = -32603;
7
+ var TOOL_USER_ERROR_BRAND = /* @__PURE__ */ Symbol.for("ampless.mcp.toolUserError");
8
+ var ToolUserError = class extends Error {
9
+ [TOOL_USER_ERROR_BRAND] = true;
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "ToolUserError";
13
+ }
14
+ };
15
+ function isToolUserError(error) {
16
+ return error instanceof Error && error[TOOL_USER_ERROR_BRAND] === true;
17
+ }
18
+ var SUPPORTED_PROTOCOL_VERSIONS = ["2025-03-26", "2024-11-05"];
19
+ var LATEST_SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
20
+ function jsonRpcResult(id, result) {
21
+ return { jsonrpc: "2.0", id, result };
22
+ }
23
+ function jsonRpcError(id, code, message, data) {
24
+ const error = { code, message };
25
+ if (data !== void 0) error.data = data;
26
+ return { jsonrpc: "2.0", id, error };
27
+ }
28
+ function isNotification(req) {
29
+ return req.id === void 0;
30
+ }
31
+ function hasValidJsonRpcId(req) {
32
+ const id = req.id;
33
+ if (id === void 0) return true;
34
+ return typeof id === "string" || typeof id === "number" && Number.isInteger(id);
35
+ }
36
+ function isPlainObject(v) {
37
+ return typeof v === "object" && v !== null && !Array.isArray(v);
38
+ }
39
+ var MAX_BATCH = 50;
40
+ function idForError(input) {
41
+ if (isPlainObject(input) && hasValidJsonRpcId(input)) {
42
+ return input.id ?? null;
43
+ }
44
+ return null;
45
+ }
46
+ function validateEnvelope(input, inBatch) {
47
+ if (!isPlainObject(input)) {
48
+ return { ok: false, error: jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Invalid Request") };
49
+ }
50
+ const jsonrpc = input.jsonrpc;
51
+ const method = input.method;
52
+ if (jsonrpc !== "2.0" || typeof method !== "string" || !hasValidJsonRpcId(input)) {
53
+ return {
54
+ ok: false,
55
+ error: jsonRpcError(idForError(input), JSON_RPC_INVALID_REQUEST, "Invalid Request")
56
+ };
57
+ }
58
+ if (inBatch && method === "initialize") {
59
+ return {
60
+ ok: false,
61
+ error: jsonRpcError(
62
+ idForError(input),
63
+ JSON_RPC_INVALID_REQUEST,
64
+ "Invalid Request: `initialize` is not allowed in a batch"
65
+ )
66
+ };
67
+ }
68
+ return { ok: true, req: input };
69
+ }
70
+ async function dispatchJsonRpcMessage(input, opts) {
71
+ const maxBatch = opts.maxBatch ?? MAX_BATCH;
72
+ if (Array.isArray(input)) {
73
+ if (input.length === 0 || input.length > maxBatch) {
74
+ return {
75
+ status: "invalid",
76
+ body: jsonRpcError(
77
+ null,
78
+ JSON_RPC_INVALID_REQUEST,
79
+ input.length === 0 ? "Invalid Request: empty batch" : `Invalid Request: batch exceeds the maximum of ${maxBatch} elements`
80
+ )
81
+ };
82
+ }
83
+ const responses = [];
84
+ for (const element of input) {
85
+ const validated2 = validateEnvelope(element, true);
86
+ if (!validated2.ok) {
87
+ responses.push(validated2.error);
88
+ continue;
89
+ }
90
+ const res2 = await dispatchJsonRpc(validated2.req, opts);
91
+ if (res2 !== null) responses.push(res2);
92
+ }
93
+ return responses.length === 0 ? { status: "no-content" } : { status: "ok", body: responses };
94
+ }
95
+ const validated = validateEnvelope(input, false);
96
+ if (!validated.ok) {
97
+ return { status: "invalid", body: validated.error };
98
+ }
99
+ const res = await dispatchJsonRpc(validated.req, opts);
100
+ return res === null ? { status: "no-content" } : { status: "ok", body: res };
101
+ }
102
+ function toolAnnotations(t) {
103
+ const annotations = {};
104
+ if (typeof t.readOnly === "boolean") annotations.readOnlyHint = t.readOnly;
105
+ if (typeof t.destructive === "boolean") annotations.destructiveHint = t.destructive;
106
+ return annotations;
107
+ }
108
+ async function dispatchJsonRpc(req, opts) {
109
+ if (!hasValidJsonRpcId(req)) {
110
+ return jsonRpcError(
111
+ null,
112
+ JSON_RPC_INVALID_REQUEST,
113
+ "Invalid Request: `id` must be a string or an integer"
114
+ );
115
+ }
116
+ const response = await dispatchMethod(req, opts);
117
+ return isNotification(req) ? null : response;
118
+ }
119
+ async function dispatchMethod(req, opts) {
120
+ const id = req.id ?? null;
121
+ switch (req.method) {
122
+ case "initialize": {
123
+ const requested = req.params?.protocolVersion;
124
+ if (typeof requested !== "string") {
125
+ return jsonRpcError(
126
+ id,
127
+ JSON_RPC_INVALID_PARAMS,
128
+ "initialize requires a string `protocolVersion` parameter"
129
+ );
130
+ }
131
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
132
+ requested
133
+ ) ? requested : LATEST_SUPPORTED_PROTOCOL_VERSION;
134
+ return jsonRpcResult(id, {
135
+ protocolVersion,
136
+ capabilities: { tools: {} },
137
+ serverInfo: opts.serverInfo
138
+ });
139
+ }
140
+ case "notifications/initialized":
141
+ return jsonRpcResult(id, {});
142
+ case "tools/list":
143
+ return jsonRpcResult(id, {
144
+ tools: opts.tools.map((t) => ({
145
+ name: t.name,
146
+ description: t.description,
147
+ inputSchema: t.inputSchema,
148
+ annotations: toolAnnotations(t)
149
+ }))
150
+ });
151
+ case "tools/call": {
152
+ const params = req.params;
153
+ if (!params || typeof params.name !== "string") {
154
+ return jsonRpcError(id, JSON_RPC_INVALID_PARAMS, "tools/call requires a `name` parameter");
155
+ }
156
+ const rawArgs = params.arguments;
157
+ if (rawArgs !== void 0 && !isPlainObject(rawArgs)) {
158
+ return jsonRpcError(
159
+ id,
160
+ JSON_RPC_INVALID_PARAMS,
161
+ "tools/call `arguments` must be an object"
162
+ );
163
+ }
164
+ const tool = opts.tools.find((t) => t.name === params.name);
165
+ if (!tool) {
166
+ return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
167
+ }
168
+ const ctx = opts.getContext();
169
+ try {
170
+ const result = await tool.handler(rawArgs ?? {}, ctx);
171
+ return jsonRpcResult(id, {
172
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
173
+ });
174
+ } catch (err) {
175
+ const rawMessage = err instanceof Error ? err.message : String(err);
176
+ const userError = isToolUserError(err);
177
+ if (!userError) {
178
+ console.error("[mcp-jsonrpc] tool dispatch failed", {
179
+ tool: params.name,
180
+ message: rawMessage
181
+ });
182
+ }
183
+ const message = userError ? rawMessage : opts.formatToolError ? opts.formatToolError(err) : rawMessage;
184
+ return jsonRpcResult(id, {
185
+ isError: true,
186
+ content: [{ type: "text", text: message }]
187
+ });
188
+ }
189
+ }
190
+ default:
191
+ return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${req.method}`);
192
+ }
193
+ }
194
+
195
+ export {
196
+ JSON_RPC_PARSE_ERROR,
197
+ JSON_RPC_INVALID_REQUEST,
198
+ JSON_RPC_METHOD_NOT_FOUND,
199
+ JSON_RPC_INVALID_PARAMS,
200
+ JSON_RPC_INTERNAL_ERROR,
201
+ ToolUserError,
202
+ isToolUserError,
203
+ SUPPORTED_PROTOCOL_VERSIONS,
204
+ LATEST_SUPPORTED_PROTOCOL_VERSION,
205
+ jsonRpcResult,
206
+ jsonRpcError,
207
+ hasValidJsonRpcId,
208
+ MAX_BATCH,
209
+ dispatchJsonRpcMessage,
210
+ dispatchJsonRpc
211
+ };
@@ -6,6 +6,18 @@ declare const JSON_RPC_INVALID_REQUEST = -32600;
6
6
  declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
7
7
  declare const JSON_RPC_INVALID_PARAMS = -32602;
8
8
  declare const JSON_RPC_INTERNAL_ERROR = -32603;
9
+ declare const TOOL_USER_ERROR_BRAND: unique symbol;
10
+ /**
11
+ * A tool failure whose message is intentionally safe to expose to MCP
12
+ * clients. Messages must not include secrets, internal details, or
13
+ * unprocessed user input.
14
+ */
15
+ declare class ToolUserError extends Error {
16
+ readonly [TOOL_USER_ERROR_BRAND] = true;
17
+ constructor(message: string);
18
+ }
19
+ /** Recognises ToolUserError instances across independently bundled entry points. */
20
+ declare function isToolUserError(error: unknown): error is ToolUserError;
9
21
  declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["2025-03-26", "2024-11-05"];
10
22
  declare const LATEST_SUPPORTED_PROTOCOL_VERSION: "2025-03-26";
11
23
  interface JsonRpcRequest {
@@ -49,7 +61,8 @@ interface JsonRpcDispatchOptions<TCtx> {
49
61
  * Converts a tool handler exception into the string surfaced to the
50
62
  * client. Omit to expose the raw error message (admin transport).
51
63
  * The public transport passes a fixed message here and logs the
52
- * detail server-side instead.
64
+ * detail server-side instead. ToolUserError messages bypass this
65
+ * formatter because they are explicitly marked as client-safe.
53
66
  */
54
67
  formatToolError?: (err: unknown) => string;
55
68
  }
@@ -65,6 +78,55 @@ interface JsonRpcDispatchOptions<TCtx> {
65
78
  * `dispatchJsonRpc` itself.
66
79
  */
67
80
  declare function hasValidJsonRpcId(req: object): boolean;
81
+ declare const MAX_BATCH = 50;
82
+ /**
83
+ * Result of dispatching an *unvalidated* decoded JSON-RPC message
84
+ * (single object or batch array) through `dispatchJsonRpcMessage`. The
85
+ * `status` tag tells the HTTP transport which response to emit:
86
+ *
87
+ * - `invalid` → 400 (top-level malformed: non-object scalar, empty
88
+ * batch, or over-size batch). `body` is the single
89
+ * error envelope to return.
90
+ * - `ok` → 200. `body` is a single response, or a batch's
91
+ * array of responses (invalid *elements* of a batch
92
+ * are error responses *inside* this array, still 200).
93
+ * - `no-content` → 202 empty body (a lone notification, or a batch
94
+ * made entirely of notifications).
95
+ */
96
+ type JsonRpcMessageResult = {
97
+ status: 'invalid';
98
+ body: JsonRpcResponse;
99
+ } | {
100
+ status: 'ok';
101
+ body: JsonRpcResponse | JsonRpcResponse[];
102
+ } | {
103
+ status: 'no-content';
104
+ };
105
+ interface JsonRpcDispatchMessageOptions<TCtx> extends JsonRpcDispatchOptions<TCtx> {
106
+ /** Batch element cap. Defaults to `MAX_BATCH` (50). */
107
+ maxBatch?: number;
108
+ }
109
+ /**
110
+ * Dispatch an *unvalidated* decoded JSON-RPC message — the single entry
111
+ * point every transport should use once it has `JSON.parse`d the body.
112
+ * Envelope validation, batch handling, and MCP method semantics are all
113
+ * centralised here so the admin (backend Lambda) and public (runtime)
114
+ * transports share one implementation; the transport keeps only HTTP
115
+ * framing (body decode, status codes, CORS, auth).
116
+ *
117
+ * Batch semantics (JSON-RPC 2.0):
118
+ * - An array is a batch. Empty / over-`maxBatch` batches are rejected
119
+ * wholesale as a single top-level INVALID_REQUEST (`status: 'invalid'`).
120
+ * - Elements are processed **sequentially** (never `Promise.all`) so a
121
+ * batch of tool calls cannot fan out into concurrent handler runs.
122
+ * - Malformed elements yield an INVALID_REQUEST response inside the
123
+ * result array (id echoed when valid, else null); `initialize` is
124
+ * not allowed as a batch element. Notifications execute but emit no
125
+ * response. Non-notification responses keep the input order.
126
+ * - A batch that yields no responses at all (all notifications) →
127
+ * `no-content`.
128
+ */
129
+ declare function dispatchJsonRpcMessage<TCtx>(input: unknown, opts: JsonRpcDispatchMessageOptions<TCtx>): Promise<JsonRpcMessageResult>;
68
130
  /**
69
131
  * Dispatch one parsed JSON-RPC request against a tool registry.
70
132
  * Returns the response envelope, or `null` for a notification (the
@@ -77,4 +139,4 @@ declare function hasValidJsonRpcId(req: object): boolean;
77
139
  */
78
140
  declare function dispatchJsonRpc<TCtx>(req: JsonRpcRequest, opts: JsonRpcDispatchOptions<TCtx>): Promise<JsonRpcResponse | null>;
79
141
 
80
- export { JSON_RPC_INTERNAL_ERROR, JSON_RPC_INVALID_PARAMS, JSON_RPC_INVALID_REQUEST, JSON_RPC_METHOD_NOT_FOUND, JSON_RPC_PARSE_ERROR, type JsonRpcDispatchOptions, type JsonRpcRequest, type JsonRpcResponse, LATEST_SUPPORTED_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, dispatchJsonRpc, hasValidJsonRpcId, jsonRpcError, jsonRpcResult };
142
+ export { JSON_RPC_INTERNAL_ERROR, JSON_RPC_INVALID_PARAMS, JSON_RPC_INVALID_REQUEST, JSON_RPC_METHOD_NOT_FOUND, JSON_RPC_PARSE_ERROR, type JsonRpcDispatchMessageOptions, type JsonRpcDispatchOptions, type JsonRpcMessageResult, type JsonRpcRequest, type JsonRpcResponse, LATEST_SUPPORTED_PROTOCOL_VERSION, MAX_BATCH, SUPPORTED_PROTOCOL_VERSIONS, ToolUserError, dispatchJsonRpc, dispatchJsonRpcMessage, hasValidJsonRpcId, isToolUserError, jsonRpcError, jsonRpcResult };
@@ -1,119 +1,20 @@
1
- // src/jsonrpc/index.ts
2
- var JSON_RPC_PARSE_ERROR = -32700;
3
- var JSON_RPC_INVALID_REQUEST = -32600;
4
- var JSON_RPC_METHOD_NOT_FOUND = -32601;
5
- var JSON_RPC_INVALID_PARAMS = -32602;
6
- var JSON_RPC_INTERNAL_ERROR = -32603;
7
- var SUPPORTED_PROTOCOL_VERSIONS = ["2025-03-26", "2024-11-05"];
8
- var LATEST_SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
9
- function jsonRpcResult(id, result) {
10
- return { jsonrpc: "2.0", id, result };
11
- }
12
- function jsonRpcError(id, code, message, data) {
13
- const error = { code, message };
14
- if (data !== void 0) error.data = data;
15
- return { jsonrpc: "2.0", id, error };
16
- }
17
- function isNotification(req) {
18
- return req.id === void 0;
19
- }
20
- function hasValidJsonRpcId(req) {
21
- const id = req.id;
22
- if (id === void 0) return true;
23
- return typeof id === "string" || typeof id === "number" && Number.isInteger(id);
24
- }
25
- function isPlainObject(v) {
26
- return typeof v === "object" && v !== null && !Array.isArray(v);
27
- }
28
- function toolAnnotations(t) {
29
- const annotations = {};
30
- if (typeof t.readOnly === "boolean") annotations.readOnlyHint = t.readOnly;
31
- if (typeof t.destructive === "boolean") annotations.destructiveHint = t.destructive;
32
- return annotations;
33
- }
34
- async function dispatchJsonRpc(req, opts) {
35
- if (!hasValidJsonRpcId(req)) {
36
- return jsonRpcError(
37
- null,
38
- JSON_RPC_INVALID_REQUEST,
39
- "Invalid Request: `id` must be a string or an integer"
40
- );
41
- }
42
- const response = await dispatchMethod(req, opts);
43
- return isNotification(req) ? null : response;
44
- }
45
- async function dispatchMethod(req, opts) {
46
- const id = req.id ?? null;
47
- switch (req.method) {
48
- case "initialize": {
49
- const requested = req.params?.protocolVersion;
50
- if (typeof requested !== "string") {
51
- return jsonRpcError(
52
- id,
53
- JSON_RPC_INVALID_PARAMS,
54
- "initialize requires a string `protocolVersion` parameter"
55
- );
56
- }
57
- const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
58
- requested
59
- ) ? requested : LATEST_SUPPORTED_PROTOCOL_VERSION;
60
- return jsonRpcResult(id, {
61
- protocolVersion,
62
- capabilities: { tools: {} },
63
- serverInfo: opts.serverInfo
64
- });
65
- }
66
- case "notifications/initialized":
67
- return jsonRpcResult(id, {});
68
- case "tools/list":
69
- return jsonRpcResult(id, {
70
- tools: opts.tools.map((t) => ({
71
- name: t.name,
72
- description: t.description,
73
- inputSchema: t.inputSchema,
74
- annotations: toolAnnotations(t)
75
- }))
76
- });
77
- case "tools/call": {
78
- const params = req.params;
79
- if (!params || typeof params.name !== "string") {
80
- return jsonRpcError(id, JSON_RPC_INVALID_PARAMS, "tools/call requires a `name` parameter");
81
- }
82
- const rawArgs = params.arguments;
83
- if (rawArgs !== void 0 && !isPlainObject(rawArgs)) {
84
- return jsonRpcError(
85
- id,
86
- JSON_RPC_INVALID_PARAMS,
87
- "tools/call `arguments` must be an object"
88
- );
89
- }
90
- const tool = opts.tools.find((t) => t.name === params.name);
91
- if (!tool) {
92
- return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
93
- }
94
- const ctx = opts.getContext();
95
- try {
96
- const result = await tool.handler(rawArgs ?? {}, ctx);
97
- return jsonRpcResult(id, {
98
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
99
- });
100
- } catch (err) {
101
- const rawMessage = err instanceof Error ? err.message : String(err);
102
- console.error("[mcp-jsonrpc] tool dispatch failed", {
103
- tool: params.name,
104
- message: rawMessage
105
- });
106
- const message = opts.formatToolError ? opts.formatToolError(err) : rawMessage;
107
- return jsonRpcResult(id, {
108
- isError: true,
109
- content: [{ type: "text", text: message }]
110
- });
111
- }
112
- }
113
- default:
114
- return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${req.method}`);
115
- }
116
- }
1
+ import {
2
+ JSON_RPC_INTERNAL_ERROR,
3
+ JSON_RPC_INVALID_PARAMS,
4
+ JSON_RPC_INVALID_REQUEST,
5
+ JSON_RPC_METHOD_NOT_FOUND,
6
+ JSON_RPC_PARSE_ERROR,
7
+ LATEST_SUPPORTED_PROTOCOL_VERSION,
8
+ MAX_BATCH,
9
+ SUPPORTED_PROTOCOL_VERSIONS,
10
+ ToolUserError,
11
+ dispatchJsonRpc,
12
+ dispatchJsonRpcMessage,
13
+ hasValidJsonRpcId,
14
+ isToolUserError,
15
+ jsonRpcError,
16
+ jsonRpcResult
17
+ } from "../chunk-4REBPD2N.js";
117
18
  export {
118
19
  JSON_RPC_INTERNAL_ERROR,
119
20
  JSON_RPC_INVALID_PARAMS,
@@ -121,9 +22,13 @@ export {
121
22
  JSON_RPC_METHOD_NOT_FOUND,
122
23
  JSON_RPC_PARSE_ERROR,
123
24
  LATEST_SUPPORTED_PROTOCOL_VERSION,
25
+ MAX_BATCH,
124
26
  SUPPORTED_PROTOCOL_VERSIONS,
27
+ ToolUserError,
125
28
  dispatchJsonRpc,
29
+ dispatchJsonRpcMessage,
126
30
  hasValidJsonRpcId,
31
+ isToolUserError,
127
32
  jsonRpcError,
128
33
  jsonRpcResult
129
34
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ ToolUserError
3
+ } from "../chunk-4REBPD2N.js";
4
+
1
5
  // src/public/types.ts
2
6
  function toPublicSummary(post) {
3
7
  const summary = {
@@ -28,33 +32,33 @@ function clampInt(v, fallback, min, max) {
28
32
  }
29
33
  function validateSlug(v) {
30
34
  if (typeof v !== "string") {
31
- throw new Error("`slug` is required and must be a string");
35
+ throw new ToolUserError("`slug` is required and must be a string");
32
36
  }
33
37
  if (v.length === 0) {
34
- throw new Error("`slug` must not be empty");
38
+ throw new ToolUserError("`slug` must not be empty");
35
39
  }
36
40
  if (v.length > MAX_SLUG_LEN) {
37
- throw new Error(`\`slug\` must be at most ${MAX_SLUG_LEN} characters`);
41
+ throw new ToolUserError(`\`slug\` must be at most ${MAX_SLUG_LEN} characters`);
38
42
  }
39
43
  return v;
40
44
  }
41
45
  function validateCursor(v) {
42
46
  if (v === void 0 || v === null) return void 0;
43
47
  if (typeof v !== "string") {
44
- throw new Error("`cursor` must be a string");
48
+ throw new ToolUserError("`cursor` must be a string");
45
49
  }
46
50
  if (v.length > MAX_CURSOR_LEN) {
47
- throw new Error(`\`cursor\` must be at most ${MAX_CURSOR_LEN} characters`);
51
+ throw new ToolUserError(`\`cursor\` must be at most ${MAX_CURSOR_LEN} characters`);
48
52
  }
49
53
  return v;
50
54
  }
51
55
  function validateQuery(v) {
52
56
  if (typeof v !== "string") {
53
- throw new Error("`query` is required and must be a string");
57
+ throw new ToolUserError("`query` is required and must be a string");
54
58
  }
55
59
  const trimmed = v.trim();
56
60
  if (trimmed.length === 0) {
57
- throw new Error("`query` must not be empty");
61
+ throw new ToolUserError("`query` must not be empty");
58
62
  }
59
63
  return trimmed.length > MAX_QUERY_LEN ? trimmed.slice(0, MAX_QUERY_LEN) : trimmed;
60
64
  }
@@ -134,7 +138,7 @@ var getPostTool = {
134
138
  const frontmatter = typeof args.frontmatter === "boolean" ? args.frontmatter : true;
135
139
  const post = await ctx.getPublishedPost(slug);
136
140
  if (!post) {
137
- throw new Error(`No published post found for slug: ${slug}`);
141
+ throw new ToolUserError("No published post found for the requested slug.");
138
142
  }
139
143
  const summary = toPublicSummary(post);
140
144
  const rendered = await ctx.postToMarkdown(post, { frontmatter });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/mcp-server",
3
- "version": "1.0.0-beta.66",
3
+ "version": "1.0.0-beta.68",
4
4
  "description": "MCP tool registry shared by @ampless/backend mcp-handler Lambda. Installed transitively via @ampless/admin / @ampless/backend; no direct install needed.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,7 +34,7 @@
34
34
  "bugs": "https://github.com/heavymoons/ampless/issues",
35
35
  "dependencies": {
36
36
  "fflate": "^0.8.3",
37
- "ampless": "1.0.0-beta.59"
37
+ "ampless": "1.0.0-beta.61"
38
38
  },
39
39
  "keywords": [
40
40
  "ampless",
@@ -45,6 +45,7 @@
45
45
  "build": "tsup",
46
46
  "dev": "tsup --watch",
47
47
  "lint": "tsc --noEmit",
48
- "test": "vitest run --passWithNoTests"
48
+ "test": "vitest run --passWithNoTests",
49
+ "test:dist": "node scripts/tool-user-error-dist-smoke.mjs"
49
50
  }
50
51
  }