@gravitykit/block-mcp 2.0.0-beta
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/.env.example +15 -0
- package/LICENSE +26 -0
- package/README.md +592 -0
- package/dist/index.cjs +52721 -0
- package/package.json +70 -0
- package/src/__tests__/fixtures/block-trees.ts +199 -0
- package/src/__tests__/fixtures/error-envelopes.ts +115 -0
- package/src/__tests__/fixtures/rest-responses.ts +280 -0
- package/src/__tests__/helpers/mock-client.ts +185 -0
- package/src/__tests__/helpers/request-matchers.ts +88 -0
- package/src/__tests__/helpers/schema-asserts.ts +132 -0
- package/src/__tests__/integration/concurrency.test.ts +129 -0
- package/src/__tests__/integration/dual-storage.test.ts +156 -0
- package/src/__tests__/integration/error-envelopes.test.ts +238 -0
- package/src/__tests__/integration/global-setup.ts +17 -0
- package/src/__tests__/integration/rate-limit.test.ts +88 -0
- package/src/__tests__/integration/read-edit-read.test.ts +141 -0
- package/src/__tests__/integration/ref-stability.test.ts +175 -0
- package/src/__tests__/integration/setup.ts +201 -0
- package/src/__tests__/tools/discovery/get_pattern.test.ts +58 -0
- package/src/__tests__/tools/discovery/get_post_info.test.ts +100 -0
- package/src/__tests__/tools/discovery/get_site_usage.test.ts +41 -0
- package/src/__tests__/tools/discovery/list_block_types.test.ts +103 -0
- package/src/__tests__/tools/discovery/list_patterns.test.ts +106 -0
- package/src/__tests__/tools/discovery/list_posts.test.ts +47 -0
- package/src/__tests__/tools/discovery/resolve_url.test.ts +69 -0
- package/src/__tests__/tools/discovery/scan_storage_modes.test.ts +34 -0
- package/src/__tests__/tools/media/upload_media.test.ts +123 -0
- package/src/__tests__/tools/mutate/edit_block_tree.test.ts +439 -0
- package/src/__tests__/tools/mutate/ref_routing.test.ts +105 -0
- package/src/__tests__/tools/patterns/insert_pattern.test.ts +117 -0
- package/src/__tests__/tools/posts/create_post.test.ts +84 -0
- package/src/__tests__/tools/posts/update_post.test.ts +93 -0
- package/src/__tests__/tools/read/get_block.test.ts +96 -0
- package/src/__tests__/tools/read/get_page_blocks.test.ts +184 -0
- package/src/__tests__/tools/read/persist_refs.test.ts +35 -0
- package/src/__tests__/tools/terms/list_terms.test.ts +91 -0
- package/src/__tests__/tools/write/delete_block.test.ts +91 -0
- package/src/__tests__/tools/write/insert_blocks.test.ts +149 -0
- package/src/__tests__/tools/write/ref_routing.test.ts +177 -0
- package/src/__tests__/tools/write/replace_block_range.test.ts +90 -0
- package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +126 -0
- package/src/__tests__/tools/write/update_block.test.ts +206 -0
- package/src/__tests__/tools/write/update_blocks.test.ts +173 -0
- package/src/__tests__/tools/yoast/yoast_bulk_update_seo.test.ts +112 -0
- package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +78 -0
- package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +105 -0
- package/src/__tests__/unit/client/ref-endpoints.test.ts +232 -0
- package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +457 -0
- package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +318 -0
- package/src/__tests__/unit/instructions.test.ts +374 -0
- package/src/__tests__/unit/preferences/enrich-block-list.test.ts +175 -0
- package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +227 -0
- package/src/client.ts +964 -0
- package/src/connect.ts +877 -0
- package/src/enrichers.ts +348 -0
- package/src/error-translator.ts +156 -0
- package/src/index.ts +450 -0
- package/src/instructions.ts +270 -0
- package/src/preferences.ts +273 -0
- package/src/tools/discovery.ts +251 -0
- package/src/tools/media.ts +75 -0
- package/src/tools/mutate.ts +243 -0
- package/src/tools/patterns.ts +94 -0
- package/src/tools/posts.ts +200 -0
- package/src/tools/read.ts +201 -0
- package/src/tools/terms.ts +44 -0
- package/src/tools/write.ts +542 -0
- package/src/tools/yoast.ts +224 -0
- package/src/types.ts +862 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GravityKit Block MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Model Context Protocol server for WordPress block-level content management.
|
|
7
|
+
* Gives AI agents full block-level CRUD with smart preference-aware guidance:
|
|
8
|
+
*
|
|
9
|
+
* - Discover available block types and patterns with preference scoring
|
|
10
|
+
* - Read page blocks as structured JSON with legacy-block annotations
|
|
11
|
+
* - Surgically edit single blocks without rewriting entire post_content
|
|
12
|
+
* - Insert patterns (synced or inline) from the site's pattern library
|
|
13
|
+
* - Full page rewrites with validation and revision tracking
|
|
14
|
+
*
|
|
15
|
+
* Connects to the gk-block-api WordPress plugin via REST API with
|
|
16
|
+
* Application Password authentication.
|
|
17
|
+
*
|
|
18
|
+
* Sub-commands:
|
|
19
|
+
* connect — Browser-Approve handoff: opens WP admin authorize URL in the
|
|
20
|
+
* browser, receives credentials via loopback callback, writes the
|
|
21
|
+
* chosen AI client's MCP config. Credentials are never logged.
|
|
22
|
+
*
|
|
23
|
+
* @see AGENTS.md and docs/specs/ for architecture and endpoint documentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
|
+
// Read the version from package.json so npm/CI bumps automatically flow
|
|
29
|
+
// into the MCP handshake — keeps the runtime version in lockstep with the
|
|
30
|
+
// published release. esbuild inlines this at bundle time.
|
|
31
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
32
|
+
import {
|
|
33
|
+
CallToolRequestSchema,
|
|
34
|
+
ListToolsRequestSchema,
|
|
35
|
+
ListResourcesRequestSchema,
|
|
36
|
+
ReadResourceRequestSchema,
|
|
37
|
+
ListPromptsRequestSchema,
|
|
38
|
+
GetPromptRequestSchema,
|
|
39
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
40
|
+
import { WordPressBlockClient } from './client.js';
|
|
41
|
+
import { getInstructions } from './instructions.js';
|
|
42
|
+
import { DISCOVERY_TOOLS, handleDiscoveryTool } from './tools/discovery.js';
|
|
43
|
+
import { READ_TOOLS, handleReadTool } from './tools/read.js';
|
|
44
|
+
import { WRITE_TOOLS, handleWriteTool } from './tools/write.js';
|
|
45
|
+
import { PATTERN_TOOLS, handlePatternTool } from './tools/patterns.js';
|
|
46
|
+
import { MUTATE_TOOLS, handleMutateTool } from './tools/mutate.js';
|
|
47
|
+
import { POST_TOOLS, handlePostTool } from './tools/posts.js';
|
|
48
|
+
import { TERM_TOOLS, handleTermTool } from './tools/terms.js';
|
|
49
|
+
import { MEDIA_TOOLS, handleMediaTool } from './tools/media.js';
|
|
50
|
+
import { YOAST_TOOLS, handleYoastTool } from './tools/yoast.js';
|
|
51
|
+
import { runConnect } from './connect.js';
|
|
52
|
+
|
|
53
|
+
// Environment variables are passed by the parent process (Claude Code, Hermes, etc.)
|
|
54
|
+
// No dotenv.config() needed — it breaks esbuild ESM bundles due to CJS dynamic require('fs').
|
|
55
|
+
|
|
56
|
+
// ============================================
|
|
57
|
+
// Initialize WordPress client
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
// Primary names (recommended): WORDPRESS_URL / WORDPRESS_USER / WORDPRESS_APP_PASSWORD.
|
|
61
|
+
// Fall back to the legacy GK_-prefixed names so existing configs keep working;
|
|
62
|
+
// emit a deprecation notice to stderr when they're used. The legacy names will
|
|
63
|
+
// be removed in a future minor release.
|
|
64
|
+
function readEnv(primary: string, legacy: string): string | undefined {
|
|
65
|
+
const fromPrimary = process.env[primary];
|
|
66
|
+
if (fromPrimary) return fromPrimary;
|
|
67
|
+
const fromLegacy = process.env[legacy];
|
|
68
|
+
if (fromLegacy) {
|
|
69
|
+
console.error(`[block-mcp] DEPRECATED: ${legacy} is deprecated; rename to ${primary} in your MCP client config.`);
|
|
70
|
+
return fromLegacy;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// Aggregate all tool definitions
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
const ALL_TOOLS = [
|
|
80
|
+
...DISCOVERY_TOOLS,
|
|
81
|
+
...READ_TOOLS,
|
|
82
|
+
...WRITE_TOOLS,
|
|
83
|
+
...PATTERN_TOOLS,
|
|
84
|
+
...MUTATE_TOOLS,
|
|
85
|
+
...POST_TOOLS,
|
|
86
|
+
...TERM_TOOLS,
|
|
87
|
+
...MEDIA_TOOLS,
|
|
88
|
+
...YOAST_TOOLS,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Tool dispatch table — name → handler function.
|
|
93
|
+
*
|
|
94
|
+
* Built once at startup from each tool group's `*_TOOLS` array paired
|
|
95
|
+
* with its `handle*Tool` dispatcher. A new tool added to any `*_TOOLS`
|
|
96
|
+
* array is automatically routable; an unrouted tool name produces a
|
|
97
|
+
* single point of failure (the Map lookup) instead of falling silently
|
|
98
|
+
* through an `else if` chain to "Unknown tool".
|
|
99
|
+
*/
|
|
100
|
+
type ToolHandler = (
|
|
101
|
+
name: string,
|
|
102
|
+
args: Record<string, unknown>,
|
|
103
|
+
client: WordPressBlockClient,
|
|
104
|
+
) => Promise<unknown>;
|
|
105
|
+
|
|
106
|
+
const TOOL_GROUPS: ReadonlyArray<{
|
|
107
|
+
tools: ReadonlyArray<{ name: string }>;
|
|
108
|
+
handle: ToolHandler;
|
|
109
|
+
}> = [
|
|
110
|
+
{ tools: DISCOVERY_TOOLS, handle: handleDiscoveryTool },
|
|
111
|
+
{ tools: READ_TOOLS, handle: handleReadTool },
|
|
112
|
+
{ tools: WRITE_TOOLS, handle: handleWriteTool },
|
|
113
|
+
{ tools: PATTERN_TOOLS, handle: handlePatternTool },
|
|
114
|
+
{ tools: MUTATE_TOOLS, handle: handleMutateTool },
|
|
115
|
+
{ tools: POST_TOOLS, handle: handlePostTool },
|
|
116
|
+
{ tools: TERM_TOOLS, handle: handleTermTool },
|
|
117
|
+
{ tools: MEDIA_TOOLS, handle: handleMediaTool },
|
|
118
|
+
{ tools: YOAST_TOOLS, handle: handleYoastTool },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const TOOL_DISPATCH = new Map<string, ToolHandler>();
|
|
122
|
+
for (const { tools, handle } of TOOL_GROUPS) {
|
|
123
|
+
for (const tool of tools) {
|
|
124
|
+
TOOL_DISPATCH.set(tool.name, handle);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================
|
|
129
|
+
// System prompt context resource
|
|
130
|
+
// ============================================
|
|
131
|
+
|
|
132
|
+
// Renamed from `block-mcp://block-preferences` — the content is workflow
|
|
133
|
+
// guidance, not the live policy itself (which lives in wp_options and is
|
|
134
|
+
// surfaced via list_block_types + per-block `preference` annotations).
|
|
135
|
+
const AGENT_GUIDE_RESOURCE_URI = 'block-mcp://agent-guide';
|
|
136
|
+
// Legacy alias — kept in the resources list for one release so existing
|
|
137
|
+
// integrations resolving the old URI don't 404.
|
|
138
|
+
const LEGACY_PREFERENCES_RESOURCE_URI = 'block-mcp://block-preferences';
|
|
139
|
+
|
|
140
|
+
const AGENT_GUIDE_CONTENT = `# Block MCP — Agent Guide
|
|
141
|
+
|
|
142
|
+
## URL → post ID resolution
|
|
143
|
+
|
|
144
|
+
NEVER run curl, wget, or any bash/shell command to hit wp-json or resolve a URL to a post ID.
|
|
145
|
+
The MCP does this for you:
|
|
146
|
+
|
|
147
|
+
- \`get_page_blocks\` accepts \`url\` as an alternative to \`post_id\`. Pass the full URL or path; the server resolves it via \`url_to_postid\`.
|
|
148
|
+
- For explicit resolution (title, post_type, edit_url before editing), call \`resolve_url\`.
|
|
149
|
+
|
|
150
|
+
If the user says "change X on https://example.com/some-page/", your first tool call should be \`get_page_blocks({ url: "...", search: "keyword" })\` or \`resolve_url({ url: "..." })\` — not a shell command.
|
|
151
|
+
|
|
152
|
+
## Moving / reordering blocks
|
|
153
|
+
|
|
154
|
+
NEVER do a move as separate \`insert_blocks\` + \`delete_block\` calls — if the delete is skipped or fails, the page ends up with an orphaned clone of the original. The atomic primitive is the \`move\` op on \`edit_block_tree\`:
|
|
155
|
+
|
|
156
|
+
- Target the source with \`ref\` (the \`gk_ref\` from \`get_page_blocks\`) or \`path\`. Prefer \`ref\` — it survives sibling shifts; paths go stale the moment any earlier block is inserted or removed.
|
|
157
|
+
- Express the destination with \`destination_ref\` or \`destination\` (path). For path destinations, use **pre-move** indexing — write the path as if the source were still in place; the server adjusts indices after the removal.
|
|
158
|
+
- Use \`count\` to move N consecutive siblings in a single op.
|
|
159
|
+
- The server rejects moves into the source itself or any of its descendants.
|
|
160
|
+
- The whole \`edit_block_tree\` call is one revision, reversible via \`revert_to_revision\`.
|
|
161
|
+
|
|
162
|
+
If you must fall back to the flat-index tools, do \`insert_blocks\` + \`delete_block\` in the same turn and re-fetch \`get_page_blocks\` afterward to confirm exactly one copy remains.
|
|
163
|
+
|
|
164
|
+
## Verifying writes
|
|
165
|
+
|
|
166
|
+
Every write echoes the canonical post-save snapshot. Use it. Do not fetch the public page to verify what saved.
|
|
167
|
+
|
|
168
|
+
- \`update_block\` always returns \`saved.inner_html\` + \`saved.attributes\` — the exact content that just landed in post_content. The write call IS the verification round-trip.
|
|
169
|
+
- \`update_blocks\` returns per-result \`saved\` only when called with \`verbose: true\` (default false to keep batch responses compact). Pass \`verbose: true\` if you need to confirm each item without a re-read.
|
|
170
|
+
- For after-the-fact re-reads of a single known block, use \`get_block({ post_id, ref })\` — returns the same \`saved\` shape, lighter than \`get_page_blocks\`.
|
|
171
|
+
|
|
172
|
+
For dynamic blocks (\`saved.is_dynamic: true\`, e.g. shortcodes, query loops, latest-posts), \`saved.inner_html\` is the stored template that runs at render time — not the rendered HTML the visitor sees. That's expected; the canonical state is the template.
|
|
173
|
+
|
|
174
|
+
## Block preferences (site-defined)
|
|
175
|
+
|
|
176
|
+
Block preference policy is configured per-site in the WordPress admin (the
|
|
177
|
+
gk-block-api Preferences option) and exposed dynamically. There is no
|
|
178
|
+
client-side hardcoded list of "good" vs "bad" namespaces.
|
|
179
|
+
|
|
180
|
+
How to discover the policy at runtime:
|
|
181
|
+
|
|
182
|
+
1. \`list_block_types\` returns blocks grouped by tier (PREFERRED / ACCEPTABLE / AVOID / LEGACY) for the current site. Use this when you need the full picture.
|
|
183
|
+
2. \`get_page_blocks\` annotates non-preferred blocks inline with \`preference.tier\` and (when configured) \`preference.suggested_replacement\`. Trust those fields — they reflect the live config.
|
|
184
|
+
3. \`insert_blocks\` rejects legacy-tier blocks with a \`legacy_block\` error that includes the rejected namespace, the suggested replacement, and a pointer back to this resource.
|
|
185
|
+
|
|
186
|
+
How to behave:
|
|
187
|
+
|
|
188
|
+
- Prefer the highest-tier blocks for new content. Defer to the server's classification rather than guessing from a namespace prefix.
|
|
189
|
+
- Reuse existing patterns before building from scratch — call \`list_patterns\` first.
|
|
190
|
+
- For patterns that need per-page customization, use \`synced: false\` to inline them.
|
|
191
|
+
- When you encounter legacy blocks on a page during a read, note them but do not replace unless asked.`;
|
|
192
|
+
|
|
193
|
+
// ============================================
|
|
194
|
+
// Handler registration
|
|
195
|
+
//
|
|
196
|
+
// All request handlers run on the server passed in by main(). Keeping
|
|
197
|
+
// this in a function instead of running at module scope means the
|
|
198
|
+
// server can be constructed AFTER fetching the per-site instructions
|
|
199
|
+
// addendum (otherwise we'd have to mutate the SDK's private
|
|
200
|
+
// `_instructions` field — see the construction note above).
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
function registerHandlers(server: McpServer, client: WordPressBlockClient): void {
|
|
204
|
+
|
|
205
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
206
|
+
return { tools: ALL_TOOLS };
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// Handler: Call tool
|
|
211
|
+
// ============================================
|
|
212
|
+
|
|
213
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
214
|
+
const { name, arguments: args } = request.params;
|
|
215
|
+
const toolArgs = (args ?? {}) as Record<string, unknown>;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const handle = TOOL_DISPATCH.get(name);
|
|
219
|
+
if (!handle) {
|
|
220
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
221
|
+
}
|
|
222
|
+
const result = await handle(name, toolArgs, client);
|
|
223
|
+
|
|
224
|
+
// Emit `structuredContent` alongside the text fallback when the tool
|
|
225
|
+
// declared an `outputSchema`. Clients that parse structured output
|
|
226
|
+
// (MCP Inspector, programmatic agents) get typed data; LLM clients
|
|
227
|
+
// still see the same JSON-stringified text.
|
|
228
|
+
const toolDef = ALL_TOOLS.find((t) => t.name === name) as { outputSchema?: unknown } | undefined;
|
|
229
|
+
const response: {
|
|
230
|
+
content: Array<{ type: string; text: string }>;
|
|
231
|
+
structuredContent?: unknown;
|
|
232
|
+
} = {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: 'text', text: JSON.stringify(result, null, 2) },
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
if (toolDef && toolDef.outputSchema !== undefined && result !== null && typeof result === 'object') {
|
|
238
|
+
response.structuredContent = result;
|
|
239
|
+
}
|
|
240
|
+
return response;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Surface the full WordPress error envelope (code, data, status) when
|
|
243
|
+
// the client decorated it. Critical for AI agents — `legacy_block`
|
|
244
|
+
// carries `suggested_replacement`, `dual_storage_requires_both`
|
|
245
|
+
// carries the policy_resource pointer, etc. Without forwarding `data`
|
|
246
|
+
// the agent only sees the prose message and re-prompts blindly.
|
|
247
|
+
const err = error as Error & {
|
|
248
|
+
wpCode?: string;
|
|
249
|
+
wpData?: unknown;
|
|
250
|
+
wpStatus?: number;
|
|
251
|
+
response?: { status?: number; data?: unknown };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
error: true,
|
|
261
|
+
tool: name,
|
|
262
|
+
message: err.message || 'Unknown error occurred',
|
|
263
|
+
code: err.wpCode,
|
|
264
|
+
statusCode: err.wpStatus ?? err.response?.status,
|
|
265
|
+
hint: err.wpData ?? null,
|
|
266
|
+
},
|
|
267
|
+
null,
|
|
268
|
+
2
|
|
269
|
+
),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ============================================
|
|
278
|
+
// Handler: List resources
|
|
279
|
+
// ============================================
|
|
280
|
+
|
|
281
|
+
server.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
282
|
+
return {
|
|
283
|
+
resources: [
|
|
284
|
+
{
|
|
285
|
+
uri: AGENT_GUIDE_RESOURCE_URI,
|
|
286
|
+
name: 'Block MCP — Agent Guide',
|
|
287
|
+
description:
|
|
288
|
+
'Editing workflow + how to discover the live block-preference policy on this site. Read this before editing pages.',
|
|
289
|
+
mimeType: 'text/plain',
|
|
290
|
+
},
|
|
291
|
+
// Legacy alias kept for one release; resolves to the same content.
|
|
292
|
+
{
|
|
293
|
+
uri: LEGACY_PREFERENCES_RESOURCE_URI,
|
|
294
|
+
name: 'Block MCP — Agent Guide (legacy URI)',
|
|
295
|
+
description: 'Renamed to block-mcp://agent-guide. Same content; kept for backwards compatibility.',
|
|
296
|
+
mimeType: 'text/plain',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ============================================
|
|
303
|
+
// Handler: Read resource
|
|
304
|
+
// ============================================
|
|
305
|
+
|
|
306
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
307
|
+
const { uri } = request.params;
|
|
308
|
+
|
|
309
|
+
if (uri === AGENT_GUIDE_RESOURCE_URI || uri === LEGACY_PREFERENCES_RESOURCE_URI) {
|
|
310
|
+
return {
|
|
311
|
+
contents: [
|
|
312
|
+
{ uri, mimeType: 'text/plain', text: AGENT_GUIDE_CONTENT },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ============================================
|
|
321
|
+
// Handler: Prompts
|
|
322
|
+
// ============================================
|
|
323
|
+
//
|
|
324
|
+
// Single canonical prompt — `edit-block-page` — that bundles the editing
|
|
325
|
+
// workflow + a one-shot reminder to call get_page_blocks first. Saves a
|
|
326
|
+
// tool call per session for clients that surface prompts in the UI.
|
|
327
|
+
|
|
328
|
+
const PROMPTS = [
|
|
329
|
+
{
|
|
330
|
+
name: 'edit-block-page',
|
|
331
|
+
description:
|
|
332
|
+
'Bundle: workflow guidance + reminder to call get_page_blocks first. Pass `url` to seed a specific page.',
|
|
333
|
+
arguments: [
|
|
334
|
+
{
|
|
335
|
+
name: 'url',
|
|
336
|
+
description: 'Optional. Full URL or path of the page being edited.',
|
|
337
|
+
required: false,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
344
|
+
prompts: PROMPTS,
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
348
|
+
const { name, arguments: args } = request.params;
|
|
349
|
+
if (name !== 'edit-block-page') {
|
|
350
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
351
|
+
}
|
|
352
|
+
const url = (args?.url as string | undefined) ?? '';
|
|
353
|
+
const seed = url
|
|
354
|
+
? `Editing target: ${url}\n\nFirst tool call: get_page_blocks({ url: ${JSON.stringify(url)}, summary_only: true }) for cheap orientation, then re-fetch with search/block_name filters as needed.\n\n`
|
|
355
|
+
: '';
|
|
356
|
+
return {
|
|
357
|
+
description: 'Workflow primer for editing a WordPress page via block-mcp.',
|
|
358
|
+
messages: [
|
|
359
|
+
{
|
|
360
|
+
role: 'user',
|
|
361
|
+
content: {
|
|
362
|
+
type: 'text',
|
|
363
|
+
text: `${seed}${AGENT_GUIDE_CONTENT}`,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
} // end registerHandlers
|
|
371
|
+
|
|
372
|
+
// ============================================
|
|
373
|
+
// Start the server
|
|
374
|
+
// ============================================
|
|
375
|
+
|
|
376
|
+
async function main(): Promise<void> {
|
|
377
|
+
// ── Node version preflight ──────────────────────────────────────────────
|
|
378
|
+
// engines.node already warns at install time, but a non-technical user who
|
|
379
|
+
// runs the connector anyway should get a clear, actionable message rather than
|
|
380
|
+
// a cryptic runtime crash on an unsupported Node.
|
|
381
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
382
|
+
if (Number.isFinite(nodeMajor) && nodeMajor < 20) {
|
|
383
|
+
console.error(
|
|
384
|
+
`Block MCP requires Node.js 20 or newer — you are running ${process.version}. ` +
|
|
385
|
+
'Please upgrade Node.js and try again: https://nodejs.org/'
|
|
386
|
+
);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── connect sub-command ─────────────────────────────────────────────────
|
|
391
|
+
// Checked first so `npx @gravitykit/block-mcp connect` works without the
|
|
392
|
+
// WORDPRESS_* env vars that the MCP server requires.
|
|
393
|
+
if (process.argv[2] === 'connect') {
|
|
394
|
+
await runConnect(process.argv.slice(3));
|
|
395
|
+
process.exit(0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── env-var validation ──────────────────────────────────────────────────
|
|
399
|
+
const WORDPRESS_URL = readEnv('WORDPRESS_URL', 'GK_SITE_URL');
|
|
400
|
+
const WORDPRESS_USER = readEnv('WORDPRESS_USER', 'GK_BLOCK_API_USER');
|
|
401
|
+
const WORDPRESS_APP_PASSWORD = readEnv('WORDPRESS_APP_PASSWORD', 'GK_BLOCK_API_APP_PASSWORD');
|
|
402
|
+
|
|
403
|
+
if (!WORDPRESS_URL || !WORDPRESS_USER || !WORDPRESS_APP_PASSWORD) {
|
|
404
|
+
console.error(
|
|
405
|
+
'Missing required environment variables: WORDPRESS_URL, WORDPRESS_USER, WORDPRESS_APP_PASSWORD'
|
|
406
|
+
);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const client = new WordPressBlockClient({
|
|
411
|
+
wordpress_url: WORDPRESS_URL,
|
|
412
|
+
auth: {
|
|
413
|
+
username: WORDPRESS_USER,
|
|
414
|
+
application_password: WORDPRESS_APP_PASSWORD,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Fetch the per-site instructions addendum BEFORE constructing the
|
|
419
|
+
// server so the initialize handshake includes the combined string
|
|
420
|
+
// from the start — no post-construction mutation of SDK internals.
|
|
421
|
+
// `getInstructions` never throws: on any failure it logs to stderr
|
|
422
|
+
// and returns the baseline only.
|
|
423
|
+
const instructions = await getInstructions(WORDPRESS_URL);
|
|
424
|
+
|
|
425
|
+
const server = new McpServer(
|
|
426
|
+
{
|
|
427
|
+
name: 'block-mcp',
|
|
428
|
+
version: pkg.version,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
capabilities: {
|
|
432
|
+
tools: {},
|
|
433
|
+
resources: {},
|
|
434
|
+
prompts: {},
|
|
435
|
+
},
|
|
436
|
+
instructions,
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
registerHandlers(server, client);
|
|
441
|
+
|
|
442
|
+
const transport = new StdioServerTransport();
|
|
443
|
+
await server.connect(transport);
|
|
444
|
+
console.error('Block MCP Server running on stdio');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
main().catch((error) => {
|
|
448
|
+
console.error('Fatal error starting Block MCP Server:', error);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
});
|