@ampless/mcp-server 1.0.0-beta.64 → 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 +66 -19
- package/README.md +64 -17
- package/dist/jsonrpc/index.d.ts +80 -0
- package/dist/jsonrpc/index.js +129 -0
- package/dist/public/index.d.ts +51 -0
- package/dist/public/index.js +228 -0
- package/dist/{index.d.ts → tools/index.d.ts} +14 -2
- package/dist/{index.js → tools/index.js} +37 -10
- package/package.json +12 -4
package/README.ja.md
CHANGED
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
## 全体の構成
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
MCP
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 クライアント +
|
|
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`
|
|
56
|
-
| `dispatchToolCall(name, args, ctx)` |
|
|
57
|
-
| `ToolDefinition
|
|
58
|
-
| `ToolContext` |
|
|
59
|
-
| `ResolvedSite` |
|
|
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 |
|
|
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
|
|
71
|
-
| `list_media` | reader |
|
|
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 |
|
|
76
|
-
| `
|
|
77
|
-
| `delete_static_file` | editor |
|
|
78
|
-
| `
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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()` |
|
|
56
|
-
| `dispatchToolCall(name, args, ctx)` |
|
|
57
|
-
| `ToolDefinition
|
|
58
|
-
| `ToolContext` |
|
|
59
|
-
| `ResolvedSite` |
|
|
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 |
|
|
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
|
|
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
|
-
| `
|
|
77
|
-
| `delete_static_file` | editor |
|
|
78
|
-
| `
|
|
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:
|
|
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.
|
|
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.
|
|
37
|
+
"ampless": "1.0.0-beta.59"
|
|
30
38
|
},
|
|
31
39
|
"keywords": [
|
|
32
40
|
"ampless",
|