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

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,19 @@ 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 { dispatchJsonRpcMessage, dispatchJsonRpc, MAX_BATCH } from '@ampless/mcp-server/jsonrpc'
75
+ import type {
76
+ JsonRpcRequest,
77
+ JsonRpcResponse,
78
+ JsonRpcMessageResult,
79
+ } from '@ampless/mcp-server/jsonrpc'
76
80
  ```
77
81
 
78
82
  | エクスポート | 説明 |
79
83
  |---|---|
80
- | `dispatchJsonRpc(req, opts)` | JSON-RPC リクエストを 1 件ツールレジストリに対して実行(`initialize` のバージョンネゴシエーション、annotations 付き `tools/list`、`tools/call`、notification 処理)。notification(`id` 無し)は method を実行しつつレスポンスの代わりに `null` を返す。`id: null` / 小数 id は `INVALID_REQUEST` で拒否 |
84
+ | `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` |
85
+ | `dispatchJsonRpc(req, opts)` | **検証済みの** JSON-RPC リクエストを 1 件ツールレジストリに対して実行(`initialize` のバージョンネゴシエーション、annotations 付き `tools/list`、`tools/call`、notification 処理)。notification(`id` 無し)は method を実行しつつレスポンスの代わりに `null` を返す。`id: null` / 小数 id は `INVALID_REQUEST` で拒否。ワイヤから来たものはエンベロープ検証と batch 対応を集約した `dispatchJsonRpcMessage` を使うこと |
86
+ | `MAX_BATCH` | batch 要素数の既定上限(`50`) |
81
87
  | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | エンベロープヘルパ + 標準エラーコード |
82
88
  | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
89
 
package/README.md CHANGED
@@ -71,13 +71,19 @@ 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 { dispatchJsonRpcMessage, dispatchJsonRpc, MAX_BATCH } from '@ampless/mcp-server/jsonrpc'
75
+ import type {
76
+ JsonRpcRequest,
77
+ JsonRpcResponse,
78
+ JsonRpcMessageResult,
79
+ } from '@ampless/mcp-server/jsonrpc'
76
80
  ```
77
81
 
78
82
  | Export | Description |
79
83
  |---|---|
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`. |
84
+ | `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`. |
85
+ | `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. |
86
+ | `MAX_BATCH` | Default batch-element cap (`50`) |
81
87
  | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | Envelope helpers + standard error codes |
82
88
  | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
89
 
@@ -65,6 +65,55 @@ interface JsonRpcDispatchOptions<TCtx> {
65
65
  * `dispatchJsonRpc` itself.
66
66
  */
67
67
  declare function hasValidJsonRpcId(req: object): boolean;
68
+ declare const MAX_BATCH = 50;
69
+ /**
70
+ * Result of dispatching an *unvalidated* decoded JSON-RPC message
71
+ * (single object or batch array) through `dispatchJsonRpcMessage`. The
72
+ * `status` tag tells the HTTP transport which response to emit:
73
+ *
74
+ * - `invalid` → 400 (top-level malformed: non-object scalar, empty
75
+ * batch, or over-size batch). `body` is the single
76
+ * error envelope to return.
77
+ * - `ok` → 200. `body` is a single response, or a batch's
78
+ * array of responses (invalid *elements* of a batch
79
+ * are error responses *inside* this array, still 200).
80
+ * - `no-content` → 202 empty body (a lone notification, or a batch
81
+ * made entirely of notifications).
82
+ */
83
+ type JsonRpcMessageResult = {
84
+ status: 'invalid';
85
+ body: JsonRpcResponse;
86
+ } | {
87
+ status: 'ok';
88
+ body: JsonRpcResponse | JsonRpcResponse[];
89
+ } | {
90
+ status: 'no-content';
91
+ };
92
+ interface JsonRpcDispatchMessageOptions<TCtx> extends JsonRpcDispatchOptions<TCtx> {
93
+ /** Batch element cap. Defaults to `MAX_BATCH` (50). */
94
+ maxBatch?: number;
95
+ }
96
+ /**
97
+ * Dispatch an *unvalidated* decoded JSON-RPC message — the single entry
98
+ * point every transport should use once it has `JSON.parse`d the body.
99
+ * Envelope validation, batch handling, and MCP method semantics are all
100
+ * centralised here so the admin (backend Lambda) and public (runtime)
101
+ * transports share one implementation; the transport keeps only HTTP
102
+ * framing (body decode, status codes, CORS, auth).
103
+ *
104
+ * Batch semantics (JSON-RPC 2.0):
105
+ * - An array is a batch. Empty / over-`maxBatch` batches are rejected
106
+ * wholesale as a single top-level INVALID_REQUEST (`status: 'invalid'`).
107
+ * - Elements are processed **sequentially** (never `Promise.all`) so a
108
+ * batch of tool calls cannot fan out into concurrent handler runs.
109
+ * - Malformed elements yield an INVALID_REQUEST response inside the
110
+ * result array (id echoed when valid, else null); `initialize` is
111
+ * not allowed as a batch element. Notifications execute but emit no
112
+ * response. Non-notification responses keep the input order.
113
+ * - A batch that yields no responses at all (all notifications) →
114
+ * `no-content`.
115
+ */
116
+ declare function dispatchJsonRpcMessage<TCtx>(input: unknown, opts: JsonRpcDispatchMessageOptions<TCtx>): Promise<JsonRpcMessageResult>;
68
117
  /**
69
118
  * Dispatch one parsed JSON-RPC request against a tool registry.
70
119
  * Returns the response envelope, or `null` for a notification (the
@@ -77,4 +126,4 @@ declare function hasValidJsonRpcId(req: object): boolean;
77
126
  */
78
127
  declare function dispatchJsonRpc<TCtx>(req: JsonRpcRequest, opts: JsonRpcDispatchOptions<TCtx>): Promise<JsonRpcResponse | null>;
79
128
 
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 };
129
+ 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, dispatchJsonRpc, dispatchJsonRpcMessage, hasValidJsonRpcId, jsonRpcError, jsonRpcResult };
@@ -25,6 +25,69 @@ function hasValidJsonRpcId(req) {
25
25
  function isPlainObject(v) {
26
26
  return typeof v === "object" && v !== null && !Array.isArray(v);
27
27
  }
28
+ var MAX_BATCH = 50;
29
+ function idForError(input) {
30
+ if (isPlainObject(input) && hasValidJsonRpcId(input)) {
31
+ return input.id ?? null;
32
+ }
33
+ return null;
34
+ }
35
+ function validateEnvelope(input, inBatch) {
36
+ if (!isPlainObject(input)) {
37
+ return { ok: false, error: jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Invalid Request") };
38
+ }
39
+ const jsonrpc = input.jsonrpc;
40
+ const method = input.method;
41
+ if (jsonrpc !== "2.0" || typeof method !== "string" || !hasValidJsonRpcId(input)) {
42
+ return {
43
+ ok: false,
44
+ error: jsonRpcError(idForError(input), JSON_RPC_INVALID_REQUEST, "Invalid Request")
45
+ };
46
+ }
47
+ if (inBatch && method === "initialize") {
48
+ return {
49
+ ok: false,
50
+ error: jsonRpcError(
51
+ idForError(input),
52
+ JSON_RPC_INVALID_REQUEST,
53
+ "Invalid Request: `initialize` is not allowed in a batch"
54
+ )
55
+ };
56
+ }
57
+ return { ok: true, req: input };
58
+ }
59
+ async function dispatchJsonRpcMessage(input, opts) {
60
+ const maxBatch = opts.maxBatch ?? MAX_BATCH;
61
+ if (Array.isArray(input)) {
62
+ if (input.length === 0 || input.length > maxBatch) {
63
+ return {
64
+ status: "invalid",
65
+ body: jsonRpcError(
66
+ null,
67
+ JSON_RPC_INVALID_REQUEST,
68
+ input.length === 0 ? "Invalid Request: empty batch" : `Invalid Request: batch exceeds the maximum of ${maxBatch} elements`
69
+ )
70
+ };
71
+ }
72
+ const responses = [];
73
+ for (const element of input) {
74
+ const validated2 = validateEnvelope(element, true);
75
+ if (!validated2.ok) {
76
+ responses.push(validated2.error);
77
+ continue;
78
+ }
79
+ const res2 = await dispatchJsonRpc(validated2.req, opts);
80
+ if (res2 !== null) responses.push(res2);
81
+ }
82
+ return responses.length === 0 ? { status: "no-content" } : { status: "ok", body: responses };
83
+ }
84
+ const validated = validateEnvelope(input, false);
85
+ if (!validated.ok) {
86
+ return { status: "invalid", body: validated.error };
87
+ }
88
+ const res = await dispatchJsonRpc(validated.req, opts);
89
+ return res === null ? { status: "no-content" } : { status: "ok", body: res };
90
+ }
28
91
  function toolAnnotations(t) {
29
92
  const annotations = {};
30
93
  if (typeof t.readOnly === "boolean") annotations.readOnlyHint = t.readOnly;
@@ -121,8 +184,10 @@ export {
121
184
  JSON_RPC_METHOD_NOT_FOUND,
122
185
  JSON_RPC_PARSE_ERROR,
123
186
  LATEST_SUPPORTED_PROTOCOL_VERSION,
187
+ MAX_BATCH,
124
188
  SUPPORTED_PROTOCOL_VERSIONS,
125
189
  dispatchJsonRpc,
190
+ dispatchJsonRpcMessage,
126
191
  hasValidJsonRpcId,
127
192
  jsonRpcError,
128
193
  jsonRpcResult
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.67",
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.60"
38
38
  },
39
39
  "keywords": [
40
40
  "ampless",