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

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
@@ -10,14 +10,16 @@
10
10
  ## 全体の構成
11
11
 
12
12
  ```
13
- MCP クライアント (.mcp.json)
14
- └── HTTP Bearer トークン (amk_...)
15
- └── mcp-handler Lambda (packages/backend/src/functions/mcp-handler.ts)
16
- └── @ampless/mcp-server/tools ← このパッケージ
17
- └── ToolDefinition[], dispatchToolCall
13
+ Admin MCP: クライアント (.mcp.json) ── Bearer amk_… ──▶ mcp-handler Lambda (@ampless/backend)
14
+ ├── @ampless/mcp-server/tools (admin ツールレジストリ)
15
+ └── @ampless/mcp-server/jsonrpc (共有 JSON-RPC dispatch)
16
+
17
+ Public MCP: 匿名クライアント ────────────────────────▶ /api/mcp ルート (@ampless/runtime)
18
+ ├── @ampless/mcp-server/public (読み取り専用ツール)
19
+ └── @ampless/mcp-server/jsonrpc (共有 JSON-RPC dispatch)
18
20
  ```
19
21
 
20
- `mcp-handler` Lambda は Bearer トークンを `McpToken` AppSync モデル(admin 専用)に照合し、`ToolContext`(GraphQL クライアント + S3 クライアント + サイトコンテキスト)を構築して、各ツール呼び出しをこのパッケージの `dispatchToolCall` に委譲します。
22
+ admin の `mcp-handler` Lambda は Bearer トークンを `McpToken` AppSync モデル(admin 専用)に照合し、`ToolContext`(GraphQL クライアント + S3 クライアント + サイトコンテキスト)を構築して、各リクエストを共有 `dispatchJsonRpc` で admin の `tools` レジストリに対して実行します。公開 runtime ルートは読み取り専用の `PublicToolContext` を注入し、同じ `dispatchJsonRpc` を `publicTools` に対して実行します — トークン不要・公開投稿のみ。
21
23
 
22
24
  エンドユーザーは管理画面経由で MCP を設定します:
23
25
 
@@ -43,6 +45,14 @@ HTTP MCP のアーキテクチャ全体については `docs/architecture/04-acc
43
45
 
44
46
  ## エクスポート
45
47
 
48
+ 3 つのサブパスエントリ:
49
+
50
+ | サブパス | 用途 |
51
+ |---|---|
52
+ | `@ampless/mcp-server/tools` | admin ツールレジストリ(トークン認証、フル read/write) |
53
+ | `@ampless/mcp-server/jsonrpc` | 両エンドポイントで共有するトランスポート非依存の JSON-RPC 2.0 dispatch |
54
+ | `@ampless/mcp-server/public` | 読み取り専用の公開ツール(匿名、公開投稿のみ) |
55
+
46
56
  ### `./tools`
47
57
 
48
58
  ```typescript
@@ -52,27 +62,64 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
52
62
 
53
63
  | エクスポート | 説明 |
54
64
  |---|---|
55
- | `getTools()` | `ToolDefinition` オブジェクトのリスト(name、description、inputSchema)を返す |
56
- | `dispatchToolCall(name, args, ctx)` | 名前でツール呼び出しをディスパッチする。不明なツール名の場合は例外をスロー |
57
- | `ToolDefinition` | 単一ツールのインターフェース(name、description、inputSchema、handler |
58
- | `ToolContext` | ランタイムコンテキストのインターフェース(graphql、storage、site) |
59
- | `ResolvedSite` | 解決済みサイトコンテキストのインターフェース(name、url、environment、siteId) |
65
+ | `tools` / `getTools()` | admin の `ToolDefinition[]` レジストリ |
66
+ | `dispatchToolCall(name, args, ctx)` | 名前でツールを引いて handler を呼ぶ(不明なら `null`) |
67
+ | `ToolDefinition<TCtx>` | 単一ツール(name、description、inputSchema、handler、`readOnly?`、`destructive?`) |
68
+ | `ToolContext` | admin ランタイムコンテキスト(graphql、storage、site) |
69
+ | `ResolvedSite` | 解決済みサイトコンテキスト(name、url、environment、siteId) |
70
+
71
+ ### `./jsonrpc`
72
+
73
+ ```typescript
74
+ import { dispatchJsonRpc } from '@ampless/mcp-server/jsonrpc'
75
+ import type { JsonRpcRequest, JsonRpcResponse } from '@ampless/mcp-server/jsonrpc'
76
+ ```
77
+
78
+ | エクスポート | 説明 |
79
+ |---|---|
80
+ | `dispatchJsonRpc(req, opts)` | JSON-RPC リクエストを 1 件ツールレジストリに対して実行(`initialize` のバージョンネゴシエーション、annotations 付き `tools/list`、`tools/call`、notification 処理)。notification(`id` 無し)は method を実行しつつレスポンスの代わりに `null` を返す。`id: null` / 小数 id は `INVALID_REQUEST` で拒否 |
81
+ | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | エンベロープヘルパ + 標準エラーコード |
82
+ | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
+
84
+ ### `./public`
85
+
86
+ ```typescript
87
+ import { publicTools } from '@ampless/mcp-server/public'
88
+ import type { PublicToolContext } from '@ampless/mcp-server/public'
89
+ ```
90
+
91
+ | エクスポート | 説明 |
92
+ |---|---|
93
+ | `publicTools` | 読み取り専用の 4 ツール(`ToolDefinition<PublicToolContext>`) |
94
+ | `PublicToolContext` | runtime が注入する最小の読み取り面(`listPublishedPosts` / `getPublishedPost` / `postToMarkdown`) |
95
+ | `toPublicSummary` | `Post` のフィールド allowlist 射影 |
60
96
 
61
97
  ## ツール一覧
62
98
 
99
+ ### Admin(`./tools`、トークン認証)
100
+
63
101
  | ツール | role | 説明 |
64
102
  |---|---|---|
65
- | `list_posts` | reader | 投稿の軽量サマリーを返す(body なし — 本文は `get_post` を使用)。検索/ソート/フィルター対応。`{ posts, total, offset, limit }` を返す |
103
+ | `list_posts` | reader | 投稿の軽量サマリー(body なし — 本文は `get_post`)。検索/ソート/フィルター対応。`{ posts, total, offset, limit }` を返す |
66
104
  | `get_post` | reader | slug または postId で単一の投稿を取得 |
67
105
  | `create_post` | editor | 新しい投稿を作成(下書きまたは公開済み) |
68
106
  | `update_post` | editor | 既存の投稿のフィールドをパッチ更新 |
69
107
  | `delete_post` | editor | 投稿を削除しタグインデックスをクリーンアップ |
70
- | `upload_media` | editor | Base64 エンコードされたバイト列を S3 にアップロードし Media レコードを作成 |
71
- | `list_media` | reader | メディア一覧を取得。`mimeType`(前方一致)/ `prefix` / `createdAfter` / `createdBefore` フィルターとページネーション対応 |
108
+ | `upload_media` | editor | Base64 バイト列を S3 にアップロードし Media レコードを作成 |
109
+ | `list_media` | reader | メディア一覧。`mimeType`(前方一致)/ `prefix` / `createdAfter` / `createdBefore` フィルターとページネーション対応 |
72
110
  | `search_media` | reader | ファイル名 / `src` / `mimeType` への部分一致でメディアを検索 |
73
- | `delete_media` | editor | メディアファイルを削除(S3 オブジェクト + Media 行)。`mediaId` または `src` で指定。`dryRun: true` で削除せずにプレビュー |
111
+ | `delete_media` | editor | メディアファイルを削除(S3 オブジェクト + Media 行)。`mediaId` または `src` で指定。`dryRun: true` でプレビュー |
74
112
  | `get_schema` | reader | CMS コンテンツスキーマを返す |
75
- | `upload_static_bundle` | editor | ビルド済み静的バンドルを S3 にアップロード |
76
- | `list_static_files` | reader | 静的バンドルファイルを一覧表示 |
77
- | `delete_static_file` | editor | 静的ファイルを S3 から削除 |
78
- | `get_site_context` | reader | 現在のサイトコンテキスト(name、url、environment)を返す |
113
+ | `upload_static_bundle` | editor | ビルド済み静的バンドル(zip)を S3 1 発でアップロード |
114
+ | `upload_static_file` | editor | 静的バンドルの S3 プレフィックスに 1 ファイルずつ差分アップロード |
115
+ | `delete_static_file` | editor | 静的バンドルの S3 プレフィックスからファイルを差分削除 |
116
+ | `commit_static_post` | editor | S3 プレフィックスから Post manifest を再構築("save" ステップ) |
117
+
118
+ ### Public(`./public`、匿名、公開投稿のみ)
119
+
120
+ | ツール | 説明 |
121
+ |---|---|
122
+ | `list_posts` | 新しい順の公開投稿サマリー 1 ページ + 不透明な `nextCursor` |
123
+ | `get_post` | slug で公開投稿 1 件、本文を `markdown` にレンダリング(10 万字超は切り詰め) |
124
+ | `search_posts` | 有界走査に対する title / slug / tags / excerpt への大小無視部分一致 |
125
+ | `list_tags` | 同じ有界走査でのタグ出現数(降順) |
package/README.md CHANGED
@@ -10,14 +10,16 @@ MCP tool registry for [ampless](https://github.com/heavymoons/ampless).
10
10
  ## How it fits together
11
11
 
12
12
  ```
13
- MCP client (.mcp.json)
14
- └── HTTP Bearer token (amk_...)
15
- └── mcp-handler Lambda (packages/backend/src/functions/mcp-handler.ts)
16
- └── @ampless/mcp-server/tools ← this package
17
- └── ToolDefinition[], dispatchToolCall
13
+ Admin MCP: client (.mcp.json) ── Bearer amk_… ──▶ mcp-handler Lambda (@ampless/backend)
14
+ ├── @ampless/mcp-server/tools (admin tool registry)
15
+ └── @ampless/mcp-server/jsonrpc (shared JSON-RPC dispatch)
16
+
17
+ Public MCP: anonymous client ──────────────────▶ /api/mcp route (@ampless/runtime)
18
+ ├── @ampless/mcp-server/public (read-only tools)
19
+ └── @ampless/mcp-server/jsonrpc (shared JSON-RPC dispatch)
18
20
  ```
19
21
 
20
- The `mcp-handler` Lambda resolves the Bearer token against the `McpToken` AppSync model (admin-only), builds a `ToolContext` (GraphQL client + S3 client + site context), and delegates each incoming tool call to `dispatchToolCall` from this package.
22
+ The admin `mcp-handler` Lambda resolves the Bearer token against the `McpToken` AppSync model (admin-only), builds a `ToolContext` (GraphQL client + S3 client + site context), and runs each request through the shared `dispatchJsonRpc` over the admin `tools` registry. The public runtime route injects a read-only `PublicToolContext` and runs the same `dispatchJsonRpc` over `publicTools` — no token, published posts only.
21
23
 
22
24
  End users configure MCP via the admin UI:
23
25
 
@@ -43,6 +45,14 @@ See `docs/architecture/04-access-layer-mcp.md` for the full HTTP MCP architectur
43
45
 
44
46
  ## Exports
45
47
 
48
+ Three subpath entries:
49
+
50
+ | Subpath | Purpose |
51
+ |---|---|
52
+ | `@ampless/mcp-server/tools` | Admin tool registry (token-authenticated, full read/write) |
53
+ | `@ampless/mcp-server/jsonrpc` | Transport-agnostic JSON-RPC 2.0 dispatch shared by both endpoints |
54
+ | `@ampless/mcp-server/public` | Read-only public tools (anonymous, published posts only) |
55
+
46
56
  ### `./tools`
47
57
 
48
58
  ```typescript
@@ -52,17 +62,45 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
52
62
 
53
63
  | Export | Description |
54
64
  |---|---|
55
- | `getTools()` | Returns the list of `ToolDefinition` objects (name, description, inputSchema) |
56
- | `dispatchToolCall(name, args, ctx)` | Dispatches a tool call by name; throws if unknown |
57
- | `ToolDefinition` | Interface for a single tool (name, description, inputSchema, handler) |
58
- | `ToolContext` | Interface for the runtime context (graphql, storage, site) |
59
- | `ResolvedSite` | Interface for the resolved site context (name, url, environment, siteId) |
65
+ | `tools` / `getTools()` | The admin `ToolDefinition[]` registry |
66
+ | `dispatchToolCall(name, args, ctx)` | Look up a tool by name and invoke its handler (returns `null` if unknown) |
67
+ | `ToolDefinition<TCtx>` | A single tool (name, description, inputSchema, handler, `readOnly?`, `destructive?`) |
68
+ | `ToolContext` | Admin runtime context (graphql, storage, site) |
69
+ | `ResolvedSite` | Resolved site context (name, url, environment, siteId) |
70
+
71
+ ### `./jsonrpc`
72
+
73
+ ```typescript
74
+ import { dispatchJsonRpc } from '@ampless/mcp-server/jsonrpc'
75
+ import type { JsonRpcRequest, JsonRpcResponse } from '@ampless/mcp-server/jsonrpc'
76
+ ```
77
+
78
+ | Export | Description |
79
+ |---|---|
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`. |
81
+ | `jsonRpcResult` / `jsonRpcError` / `JSON_RPC_*` | Envelope helpers + standard error codes |
82
+ | `SUPPORTED_PROTOCOL_VERSIONS` | `['2025-03-26', '2024-11-05']` |
83
+
84
+ ### `./public`
85
+
86
+ ```typescript
87
+ import { publicTools } from '@ampless/mcp-server/public'
88
+ import type { PublicToolContext } from '@ampless/mcp-server/public'
89
+ ```
90
+
91
+ | Export | Description |
92
+ |---|---|
93
+ | `publicTools` | The four read-only `ToolDefinition<PublicToolContext>` tools |
94
+ | `PublicToolContext` | The minimal read-only surface the runtime injects (`listPublishedPosts` / `getPublishedPost` / `postToMarkdown`) |
95
+ | `toPublicSummary` | Explicit field-allowlist projection of a `Post` |
60
96
 
61
97
  ## Tools
62
98
 
99
+ ### Admin (`./tools`, token-authenticated)
100
+
63
101
  | Tool | Role | Description |
64
102
  |---|---|---|
65
- | `list_posts` | reader | Returns lightweight post summaries (no body — use `get_post` for content) with search / sort / filters. Returns `{ posts, total, offset, limit }` |
103
+ | `list_posts` | reader | Lightweight post summaries (no body — use `get_post`) with search / sort / filters. Returns `{ posts, total, offset, limit }` |
66
104
  | `get_post` | reader | Fetch a single post by slug or postId |
67
105
  | `create_post` | editor | Create a new post (draft or published) |
68
106
  | `update_post` | editor | Patch fields on an existing post |
@@ -70,9 +108,18 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
70
108
  | `upload_media` | editor | Upload base64-encoded bytes to S3 and create a Media record |
71
109
  | `list_media` | reader | List media with optional `mimeType` (prefix) / `prefix` / `createdAfter` / `createdBefore` filters + pagination |
72
110
  | `search_media` | reader | Search media by substring across filename / `src` / `mimeType` |
73
- | `delete_media` | editor | Delete a media file (S3 object + Media row). Pass `mediaId` or `src`; `dryRun: true` previews without deleting |
111
+ | `delete_media` | editor | Delete a media file (S3 object + Media row). Pass `mediaId` or `src`; `dryRun: true` previews |
74
112
  | `get_schema` | reader | Return the CMS content schema |
75
- | `upload_static_bundle` | editor | Upload a pre-built static bundle to S3 |
76
- | `list_static_files` | reader | List static bundle files |
77
- | `delete_static_file` | editor | Delete a static file from S3 |
78
- | `get_site_context` | reader | Return current site context (name, url, environment) |
113
+ | `upload_static_bundle` | editor | Upload a pre-built static bundle (zip) to S3 in one shot |
114
+ | `upload_static_file` | editor | Incrementally write a single file into a static bundle's S3 prefix |
115
+ | `delete_static_file` | editor | Incrementally delete a file from a static bundle's S3 prefix |
116
+ | `commit_static_post` | editor | Rebuild the Post manifest from the S3 prefix (the "save" step) |
117
+
118
+ ### Public (`./public`, anonymous, published posts only)
119
+
120
+ | Tool | Description |
121
+ |---|---|
122
+ | `list_posts` | One page of newest-first published-post summaries + opaque `nextCursor` |
123
+ | `get_post` | A single published post by slug, body rendered to `markdown` (truncated past 100k chars) |
124
+ | `search_posts` | Case-insensitive substring over title / slug / tags / excerpt across a bounded scan |
125
+ | `list_tags` | Tag occurrence counts (descending) over the same bounded scan |
@@ -0,0 +1,80 @@
1
+ import { ToolDefinition } from '../tools/index.js';
2
+ import 'ampless';
3
+
4
+ declare const JSON_RPC_PARSE_ERROR = -32700;
5
+ declare const JSON_RPC_INVALID_REQUEST = -32600;
6
+ declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
7
+ declare const JSON_RPC_INVALID_PARAMS = -32602;
8
+ declare const JSON_RPC_INTERNAL_ERROR = -32603;
9
+ declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["2025-03-26", "2024-11-05"];
10
+ declare const LATEST_SUPPORTED_PROTOCOL_VERSION: "2025-03-26";
11
+ interface JsonRpcRequest {
12
+ jsonrpc: '2.0';
13
+ /**
14
+ * Absent (`undefined`) marks a JSON-RPC *notification*: per spec the
15
+ * server must not send any response (the method still executes). MCP
16
+ * forbids `id: null`, and JSON-RPC numeric ids SHOULD NOT contain
17
+ * fractional parts — `hasValidJsonRpcId` rejects both as
18
+ * INVALID_REQUEST before dispatch.
19
+ */
20
+ id?: number | string;
21
+ method: string;
22
+ params?: Record<string, unknown>;
23
+ }
24
+ interface JsonRpcResponse {
25
+ jsonrpc: '2.0';
26
+ id: number | string | null;
27
+ result?: unknown;
28
+ error?: {
29
+ code: number;
30
+ message: string;
31
+ data?: unknown;
32
+ };
33
+ }
34
+ declare function jsonRpcResult(id: JsonRpcResponse['id'], result: unknown): JsonRpcResponse;
35
+ declare function jsonRpcError(id: JsonRpcResponse['id'], code: number, message: string, data?: unknown): JsonRpcResponse;
36
+ interface JsonRpcDispatchOptions<TCtx> {
37
+ tools: readonly ToolDefinition<TCtx>[];
38
+ /**
39
+ * Resolves the tool context. Called lazily — only on a valid
40
+ * `tools/call` for a known tool — so `initialize` / `tools/list`
41
+ * never pay for client construction.
42
+ */
43
+ getContext: () => TCtx;
44
+ serverInfo: {
45
+ name: string;
46
+ version: string;
47
+ };
48
+ /**
49
+ * Converts a tool handler exception into the string surfaced to the
50
+ * client. Omit to expose the raw error message (admin transport).
51
+ * The public transport passes a fixed message here and logs the
52
+ * detail server-side instead.
53
+ */
54
+ formatToolError?: (err: unknown) => string;
55
+ }
56
+ /**
57
+ * Validates the `id` member of a decoded request envelope. A valid id
58
+ * is a string or an integer number; an *absent* id is also valid (it
59
+ * marks a notification — JSON cannot express `undefined`, so a
60
+ * present-but-undefined id from an in-process caller is treated the
61
+ * same). MCP explicitly forbids `id: null`, and the JSON-RPC 2.0 spec
62
+ * says numeric ids SHOULD NOT contain fractional parts — both are
63
+ * rejected (callers map `false` to INVALID_REQUEST). Exported so each
64
+ * transport's pre-dispatch envelope check enforces the same rule as
65
+ * `dispatchJsonRpc` itself.
66
+ */
67
+ declare function hasValidJsonRpcId(req: object): boolean;
68
+ /**
69
+ * Dispatch one parsed JSON-RPC request against a tool registry.
70
+ * Returns the response envelope, or `null` for a notification (the
71
+ * caller maps that to an empty 202/no-body HTTP response).
72
+ *
73
+ * Notification suppression is centralised here: a notification (id
74
+ * absent) still *executes* its method — including a `tools/call`
75
+ * handler and its side effects — but the response envelope is
76
+ * discarded, per the JSON-RPC 2.0 spec.
77
+ */
78
+ declare function dispatchJsonRpc<TCtx>(req: JsonRpcRequest, opts: JsonRpcDispatchOptions<TCtx>): Promise<JsonRpcResponse | null>;
79
+
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 };
@@ -0,0 +1,129 @@
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
+ }
117
+ export {
118
+ JSON_RPC_INTERNAL_ERROR,
119
+ JSON_RPC_INVALID_PARAMS,
120
+ JSON_RPC_INVALID_REQUEST,
121
+ JSON_RPC_METHOD_NOT_FOUND,
122
+ JSON_RPC_PARSE_ERROR,
123
+ LATEST_SUPPORTED_PROTOCOL_VERSION,
124
+ SUPPORTED_PROTOCOL_VERSIONS,
125
+ dispatchJsonRpc,
126
+ hasValidJsonRpcId,
127
+ jsonRpcError,
128
+ jsonRpcResult
129
+ };
@@ -0,0 +1,51 @@
1
+ import { ToolDefinition } from '../tools/index.js';
2
+ import { Post } from 'ampless';
3
+
4
+ /**
5
+ * The minimal, read-only surface the public MCP tools need. The runtime
6
+ * (`@ampless/runtime`, PR-8) injects a concrete implementation backed by
7
+ * its apiKey-mode `listPublishedPosts` / `getPublishedPost` (which strip
8
+ * drafts server-side) and `postToMarkdown`.
9
+ *
10
+ * `listPostsByTag` is intentionally excluded: `list_tags` is served by
11
+ * scanning + aggregating the published index, so the tool layer never
12
+ * needs a tag-name-keyed lookup. Keeping the injected surface to three
13
+ * methods minimises PR-8's wiring.
14
+ */
15
+ interface PublicToolContext {
16
+ listPublishedPosts(opts: {
17
+ limit?: number;
18
+ nextToken?: string;
19
+ }): Promise<{
20
+ items: Post[];
21
+ nextToken: string | null;
22
+ }>;
23
+ getPublishedPost(slug: string): Promise<Post | null>;
24
+ postToMarkdown(post: Post, opts?: {
25
+ frontmatter?: boolean;
26
+ }): Promise<string>;
27
+ }
28
+ /**
29
+ * Field allowlist surfaced to anonymous callers. Deliberately omits
30
+ * `postId`, `status`, `metadata`, and `body` — see `toPublicSummary`.
31
+ */
32
+ interface PublicPostSummary {
33
+ slug: string;
34
+ title: string;
35
+ excerpt?: string;
36
+ tags?: string[];
37
+ publishedAt?: string;
38
+ updatedAt?: string;
39
+ format: string;
40
+ }
41
+ /**
42
+ * Explicit pick — the core of the leakage defence. Never spread a
43
+ * `Post` into the response: only the allowlisted fields below reach an
44
+ * anonymous caller. `postId` / `status` / `metadata` / `body` are never
45
+ * copied.
46
+ */
47
+ declare function toPublicSummary(post: Post): PublicPostSummary;
48
+
49
+ declare const publicTools: readonly ToolDefinition<PublicToolContext>[];
50
+
51
+ export { type PublicPostSummary, type PublicToolContext, publicTools, toPublicSummary };
@@ -0,0 +1,228 @@
1
+ // src/public/types.ts
2
+ function toPublicSummary(post) {
3
+ const summary = {
4
+ slug: post.slug,
5
+ title: post.title,
6
+ format: post.format
7
+ };
8
+ if (post.excerpt !== void 0) summary.excerpt = post.excerpt;
9
+ if (post.tags !== void 0) summary.tags = post.tags;
10
+ if (post.publishedAt !== void 0) summary.publishedAt = post.publishedAt;
11
+ if (post.updatedAt !== void 0) summary.updatedAt = post.updatedAt;
12
+ return summary;
13
+ }
14
+
15
+ // src/public/shared.ts
16
+ import { collectBounded } from "ampless";
17
+ var MAX_BODY_CHARS = 1e5;
18
+ var SEARCH_SCAN_LIMIT = 200;
19
+ var PUBLIC_PAGE_SIZE_CAP = 50;
20
+ var PUBLIC_SCAN_MAX_PAGES = 5;
21
+ var MAX_SLUG_LEN = 512;
22
+ var MAX_CURSOR_LEN = 4096;
23
+ var MAX_QUERY_LEN = 256;
24
+ function clampInt(v, fallback, min, max) {
25
+ const n = Number(v);
26
+ if (!Number.isFinite(n)) return fallback;
27
+ return Math.min(max, Math.max(min, Math.trunc(n)));
28
+ }
29
+ function validateSlug(v) {
30
+ if (typeof v !== "string") {
31
+ throw new Error("`slug` is required and must be a string");
32
+ }
33
+ if (v.length === 0) {
34
+ throw new Error("`slug` must not be empty");
35
+ }
36
+ if (v.length > MAX_SLUG_LEN) {
37
+ throw new Error(`\`slug\` must be at most ${MAX_SLUG_LEN} characters`);
38
+ }
39
+ return v;
40
+ }
41
+ function validateCursor(v) {
42
+ if (v === void 0 || v === null) return void 0;
43
+ if (typeof v !== "string") {
44
+ throw new Error("`cursor` must be a string");
45
+ }
46
+ if (v.length > MAX_CURSOR_LEN) {
47
+ throw new Error(`\`cursor\` must be at most ${MAX_CURSOR_LEN} characters`);
48
+ }
49
+ return v;
50
+ }
51
+ function validateQuery(v) {
52
+ if (typeof v !== "string") {
53
+ throw new Error("`query` is required and must be a string");
54
+ }
55
+ const trimmed = v.trim();
56
+ if (trimmed.length === 0) {
57
+ throw new Error("`query` must not be empty");
58
+ }
59
+ return trimmed.length > MAX_QUERY_LEN ? trimmed.slice(0, MAX_QUERY_LEN) : trimmed;
60
+ }
61
+ async function scanRecentPublished(ctx) {
62
+ const { items, truncated } = await collectBounded(
63
+ (args) => ctx.listPublishedPosts({ limit: args.limit, nextToken: args.nextToken }),
64
+ {
65
+ limit: SEARCH_SCAN_LIMIT,
66
+ pageSizeCap: PUBLIC_PAGE_SIZE_CAP,
67
+ maxPages: PUBLIC_SCAN_MAX_PAGES
68
+ }
69
+ );
70
+ return { posts: items, scanTruncated: truncated !== null };
71
+ }
72
+
73
+ // src/public/list-posts.ts
74
+ var DEFAULT_LIMIT = 20;
75
+ var MIN_LIMIT = 1;
76
+ var MAX_LIMIT = 50;
77
+ var listPostsSchema = {
78
+ type: "object",
79
+ properties: {
80
+ limit: {
81
+ type: "integer",
82
+ minimum: MIN_LIMIT,
83
+ maximum: MAX_LIMIT,
84
+ description: `Max posts to return (${MIN_LIMIT}\u2013${MAX_LIMIT}, default ${DEFAULT_LIMIT})`
85
+ },
86
+ cursor: {
87
+ type: "string",
88
+ maxLength: MAX_CURSOR_LEN,
89
+ description: "Opaque pagination cursor from a previous call (`nextCursor`)"
90
+ }
91
+ }
92
+ };
93
+ var listPostsTool = {
94
+ name: "list_posts",
95
+ description: "List published posts, newest first (read-only, published posts only). Returns lightweight summaries (no body \u2014 use get_post for content) and an opaque `nextCursor` for pagination. Args: `limit` (1\u201350, default 20), `cursor` (from a previous response).",
96
+ inputSchema: listPostsSchema,
97
+ readOnly: true,
98
+ destructive: false,
99
+ handler: async (args, ctx) => {
100
+ const limit = clampInt(args.limit, DEFAULT_LIMIT, MIN_LIMIT, MAX_LIMIT);
101
+ const cursor = validateCursor(args.cursor);
102
+ const { items, nextToken } = await ctx.listPublishedPosts({ limit, nextToken: cursor });
103
+ return {
104
+ posts: items.map(toPublicSummary),
105
+ nextCursor: nextToken
106
+ };
107
+ }
108
+ };
109
+
110
+ // src/public/get-post.ts
111
+ var getPostSchema = {
112
+ type: "object",
113
+ properties: {
114
+ slug: {
115
+ type: "string",
116
+ maxLength: MAX_SLUG_LEN,
117
+ description: "Slug of the published post to fetch"
118
+ },
119
+ frontmatter: {
120
+ type: "boolean",
121
+ description: "Include YAML frontmatter in the markdown (default true)"
122
+ }
123
+ },
124
+ required: ["slug"]
125
+ };
126
+ var getPostTool = {
127
+ name: "get_post",
128
+ description: "Fetch a single published post by slug (read-only, published posts only). Returns the summary fields plus the post body rendered to `markdown`. Args: `slug` (required), `frontmatter` (default true). Errors if no published post matches the slug. Very long bodies are truncated (`truncated: true`).",
129
+ inputSchema: getPostSchema,
130
+ readOnly: true,
131
+ destructive: false,
132
+ handler: async (args, ctx) => {
133
+ const slug = validateSlug(args.slug);
134
+ const frontmatter = typeof args.frontmatter === "boolean" ? args.frontmatter : true;
135
+ const post = await ctx.getPublishedPost(slug);
136
+ if (!post) {
137
+ throw new Error(`No published post found for slug: ${slug}`);
138
+ }
139
+ const summary = toPublicSummary(post);
140
+ const rendered = await ctx.postToMarkdown(post, { frontmatter });
141
+ const truncated = rendered.length > MAX_BODY_CHARS;
142
+ const markdown = truncated ? rendered.slice(0, MAX_BODY_CHARS) : rendered;
143
+ return { ...summary, markdown, truncated };
144
+ }
145
+ };
146
+
147
+ // src/public/search-posts.ts
148
+ var DEFAULT_LIMIT2 = 10;
149
+ var MIN_LIMIT2 = 1;
150
+ var MAX_LIMIT2 = 20;
151
+ var searchPostsSchema = {
152
+ type: "object",
153
+ properties: {
154
+ query: {
155
+ type: "string",
156
+ minLength: 1,
157
+ maxLength: MAX_QUERY_LEN,
158
+ description: "Case-insensitive substring matched against title / slug / tags / excerpt"
159
+ },
160
+ limit: {
161
+ type: "integer",
162
+ minimum: MIN_LIMIT2,
163
+ maximum: MAX_LIMIT2,
164
+ description: `Max matches to return (${MIN_LIMIT2}\u2013${MAX_LIMIT2}, default ${DEFAULT_LIMIT2})`
165
+ }
166
+ },
167
+ required: ["query"]
168
+ };
169
+ function matchesQuery(post, needle) {
170
+ const haystacks = [post.title, post.slug];
171
+ if (post.excerpt) haystacks.push(post.excerpt);
172
+ if (post.tags) haystacks.push(...post.tags);
173
+ return haystacks.some((h) => h.toLowerCase().includes(needle));
174
+ }
175
+ var searchPostsTool = {
176
+ name: "search_posts",
177
+ description: "Search published posts by case-insensitive substring over title / slug / tags / excerpt (read-only, published posts only; body text is not searched). Scans a bounded window of the most recent posts. Args: `query` (required, 1\u2013256 chars), `limit` (1\u201320, default 10). `scanTruncated: true` means older posts were beyond the scan window.",
178
+ inputSchema: searchPostsSchema,
179
+ readOnly: true,
180
+ destructive: false,
181
+ handler: async (args, ctx) => {
182
+ const query = validateQuery(args.query);
183
+ const limit = clampInt(args.limit, DEFAULT_LIMIT2, MIN_LIMIT2, MAX_LIMIT2);
184
+ const needle = query.toLowerCase();
185
+ const { posts, scanTruncated } = await scanRecentPublished(ctx);
186
+ const matches = posts.filter((post) => matchesQuery(post, needle)).slice(0, limit);
187
+ return {
188
+ posts: matches.map(toPublicSummary),
189
+ scanTruncated
190
+ };
191
+ }
192
+ };
193
+
194
+ // src/public/list-tags.ts
195
+ var listTagsSchema = {
196
+ type: "object",
197
+ properties: {}
198
+ };
199
+ var listTagsTool = {
200
+ name: "list_tags",
201
+ description: "List tags used by published posts with occurrence counts, most frequent first (read-only, published posts only). Aggregated over a bounded scan of the most recent posts. Returns `{ tags: [{ tag, count }], scanTruncated }`; `scanTruncated: true` means counts cover only the scan window.",
202
+ inputSchema: listTagsSchema,
203
+ readOnly: true,
204
+ destructive: false,
205
+ handler: async (_args, ctx) => {
206
+ const { posts, scanTruncated } = await scanRecentPublished(ctx);
207
+ const counts = /* @__PURE__ */ new Map();
208
+ for (const post of posts) {
209
+ for (const tag of post.tags ?? []) {
210
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
211
+ }
212
+ }
213
+ const tags = [...counts.entries()].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
214
+ return { tags, scanTruncated };
215
+ }
216
+ };
217
+
218
+ // src/public/index.ts
219
+ var publicTools = [
220
+ listPostsTool,
221
+ getPostTool,
222
+ searchPostsTool,
223
+ listTagsTool
224
+ ];
225
+ export {
226
+ publicTools,
227
+ toPublicSummary
228
+ };
@@ -128,12 +128,24 @@ declare function extractZipFromBuffer(buffer: Uint8Array, opts?: ExtractZipOptio
128
128
  */
129
129
  declare function decodeUtf8(data: Uint8Array): string;
130
130
 
131
- interface ToolDefinition {
131
+ interface ToolDefinition<TCtx = ToolContext> {
132
132
  name: string;
133
133
  description: string;
134
134
  inputSchema: Record<string, unknown>;
135
- handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
135
+ handler: (args: Record<string, unknown>, ctx: TCtx) => Promise<unknown>;
136
+ /**
137
+ * `true` when the tool can destroy or irreversibly overwrite existing
138
+ * state. Surfaced as the MCP `destructiveHint` annotation. Left
139
+ * `undefined` only for tools that have not been classified — the
140
+ * shared dispatch then omits the hint so the spec default (`true`,
141
+ * the safe side) applies.
142
+ */
136
143
  destructive?: boolean;
144
+ /**
145
+ * `true` when the tool only reads and never mutates state. Surfaced
146
+ * as the MCP `readOnlyHint` annotation.
147
+ */
148
+ readOnly?: boolean;
137
149
  }
138
150
  declare const tools: ToolDefinition[];
139
151
  /**
@@ -1731,50 +1731,66 @@ var tools = [
1731
1731
  name: "list_posts",
1732
1732
  description: "Returns lightweight post summaries (no body \u2014 use get_post for content) with search / sort / filters. `total` reflects the filtered count. Supports query (substring over title/slug/tags), tag (exact), status, sort, limit (1\u2013100, default 20), offset (default 0).",
1733
1733
  inputSchema: listPostsSchema,
1734
- handler: (args, ctx) => listPosts(ctx.graphql, args)
1734
+ handler: (args, ctx) => listPosts(ctx.graphql, args),
1735
+ readOnly: true,
1736
+ destructive: false
1735
1737
  },
1736
1738
  {
1737
1739
  name: "get_post",
1738
1740
  description: "Fetch a single post by slug or postId. Returns null if not found.",
1739
1741
  inputSchema: getPostSchema,
1740
- handler: (args, ctx) => getPost(ctx.graphql, args)
1742
+ handler: (args, ctx) => getPost(ctx.graphql, args),
1743
+ readOnly: true,
1744
+ destructive: false
1741
1745
  },
1742
1746
  {
1743
1747
  name: "create_post",
1744
1748
  description: 'Create a new post. Title and slug are required. Body shape depends on format: tiptap=JSON node tree, markdown=source string, html=raw HTML string. Defaults to status=draft. Pass `metadata: { no_layout: true }` alongside format=html to publish the body as a bare HTML page with no theme chrome (middleware rewrites the /<slug> request to the internal bare-HTML handler). Pass `metadata: { cache: "deep" | "hot" }` to override the default cooldown-based cache strategy \u2014 see get_schema.notes.cacheStrategy for details.',
1745
1749
  inputSchema: createPostSchema,
1746
- handler: (args, ctx) => createPost(ctx.graphql, args)
1750
+ handler: (args, ctx) => createPost(ctx.graphql, args),
1751
+ readOnly: false,
1752
+ destructive: false
1747
1753
  },
1748
1754
  {
1749
1755
  name: "update_post",
1750
1756
  description: "Update an existing post by postId. Only the fields you pass are changed. Tag list / publishedAt changes also update the PostTag denormalized index. Passing `metadata` REPLACES the existing object \u2014 call get_post first if you only want to add or change one key.",
1751
1757
  inputSchema: updatePostSchema,
1752
- handler: (args, ctx) => updatePost(ctx.graphql, args)
1758
+ handler: (args, ctx) => updatePost(ctx.graphql, args),
1759
+ readOnly: false,
1760
+ // Overwrites an existing post's fields — not a pure additive write.
1761
+ destructive: true
1753
1762
  },
1754
1763
  {
1755
1764
  name: "delete_post",
1756
1765
  description: "Delete a post by postId. Also drops associated PostTag index entries.",
1757
1766
  inputSchema: deletePostSchema,
1758
1767
  handler: (args, ctx) => deletePost(ctx.graphql, args),
1768
+ readOnly: false,
1759
1769
  destructive: true
1760
1770
  },
1761
1771
  {
1762
1772
  name: "upload_media",
1763
1773
  description: "Upload a file to the site's media S3 bucket. Pass base64-encoded bytes; the server stores them verbatim under public/media/YYYY/MM/. Returns the public URL and Media record. The server does not transcode \u2014 pre-process (e.g. resize/webp) on the client.",
1764
1774
  inputSchema: uploadMediaSchema,
1765
- handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), args)
1775
+ handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), args),
1776
+ readOnly: false,
1777
+ destructive: false
1766
1778
  },
1767
1779
  {
1768
1780
  name: "list_media",
1769
1781
  description: 'List media files in the CMS. Optional filters: `mimeType` (prefix match \u2014 "image/" matches all images, "image/png" only PNG), `prefix` (S3 key prefix on `src`, e.g. "public/media/2024/"), `createdAfter` / `createdBefore` (ISO 8601 date range). Returns up to `limit` rows (default 20) \u2014 each `{ mediaId, src, url, mimeType, size, createdAt, updatedAt }` \u2014 plus a `nextToken` cursor. Note: filters apply after the page read, so a page may return fewer than `limit` rows while a `nextToken` remains \u2014 follow the cursor. Use this to find media to delete without remembering `upload_media` responses.',
1770
1782
  inputSchema: listMediaSchema,
1771
- handler: (args, ctx) => listMedia(ctx.graphql, ctx.storage(), args)
1783
+ handler: (args, ctx) => listMedia(ctx.graphql, ctx.storage(), args),
1784
+ readOnly: true,
1785
+ destructive: false
1772
1786
  },
1773
1787
  {
1774
1788
  name: "search_media",
1775
1789
  description: 'Search media by substring (case-sensitive `contains`) across `src` (which includes the filename) and `mimeType`. Pass `query` (e.g. "logo", ".png", "image/png"). Walks DynamoDB pages internally until it collects at least `limit` matches (default 20), exhausts the table, or hits its page cap \u2014 so `limit` is a soft target and the result may run slightly past it. Returns the same row shape as `list_media` plus `nextToken`; `truncated: true` means the page cap was hit with more to scan (pass `nextToken` back to continue).',
1776
1790
  inputSchema: searchMediaSchema,
1777
- handler: (args, ctx) => searchMedia(ctx.graphql, ctx.storage(), args)
1791
+ handler: (args, ctx) => searchMedia(ctx.graphql, ctx.storage(), args),
1792
+ readOnly: true,
1793
+ destructive: false
1778
1794
  },
1779
1795
  {
1780
1796
  name: "delete_media",
@@ -1785,13 +1801,16 @@ var tools = [
1785
1801
  ctx.storage(),
1786
1802
  args
1787
1803
  ),
1804
+ readOnly: false,
1788
1805
  destructive: true
1789
1806
  },
1790
1807
  {
1791
1808
  name: "get_schema",
1792
1809
  description: "Returns the CMS content schema (Post/Page/Media field shapes, format enum, notes). Useful as the first call to understand what fields are available.",
1793
1810
  inputSchema: getSchemaSchema,
1794
- handler: async () => getSchema()
1811
+ handler: async () => getSchema(),
1812
+ readOnly: true,
1813
+ destructive: false
1795
1814
  },
1796
1815
  {
1797
1816
  name: "upload_static_bundle",
@@ -1802,6 +1821,7 @@ var tools = [
1802
1821
  ctx.storage(),
1803
1822
  args
1804
1823
  ),
1824
+ readOnly: false,
1805
1825
  destructive: true
1806
1826
  },
1807
1827
  {
@@ -1811,7 +1831,10 @@ var tools = [
1811
1831
  handler: (args, ctx) => uploadStaticFile(
1812
1832
  ctx.storage(),
1813
1833
  args
1814
- )
1834
+ ),
1835
+ readOnly: false,
1836
+ // Overwrites whatever file currently sits at that S3 key.
1837
+ destructive: true
1815
1838
  },
1816
1839
  {
1817
1840
  name: "delete_static_file",
@@ -1821,6 +1844,7 @@ var tools = [
1821
1844
  ctx.storage(),
1822
1845
  args
1823
1846
  ),
1847
+ readOnly: false,
1824
1848
  destructive: true
1825
1849
  },
1826
1850
  {
@@ -1831,7 +1855,10 @@ var tools = [
1831
1855
  ctx.graphql,
1832
1856
  ctx.storage(),
1833
1857
  args
1834
- )
1858
+ ),
1859
+ readOnly: false,
1860
+ // Rebuilds (overwrites) the existing Post row's manifest.
1861
+ destructive: true
1835
1862
  }
1836
1863
  ];
1837
1864
  async function dispatchToolCall(name, args, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/mcp-server",
3
- "version": "1.0.0-beta.65",
3
+ "version": "1.0.0-beta.66",
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"
@@ -9,8 +9,16 @@
9
9
  "type": "module",
10
10
  "exports": {
11
11
  "./tools": {
12
- "import": "./dist/index.js",
13
- "types": "./dist/index.d.ts"
12
+ "import": "./dist/tools/index.js",
13
+ "types": "./dist/tools/index.d.ts"
14
+ },
15
+ "./jsonrpc": {
16
+ "import": "./dist/jsonrpc/index.js",
17
+ "types": "./dist/jsonrpc/index.d.ts"
18
+ },
19
+ "./public": {
20
+ "import": "./dist/public/index.js",
21
+ "types": "./dist/public/index.d.ts"
14
22
  }
15
23
  },
16
24
  "files": [
@@ -26,7 +34,7 @@
26
34
  "bugs": "https://github.com/heavymoons/ampless/issues",
27
35
  "dependencies": {
28
36
  "fflate": "^0.8.3",
29
- "ampless": "1.0.0-beta.58"
37
+ "ampless": "1.0.0-beta.59"
30
38
  },
31
39
  "keywords": [
32
40
  "ampless",