Skip to content

Commit e1c5cb0

Browse files
committed
feat(mcp): add read-only OOXML structural tools (Phase 4)
Six new MCP tools, gated by the ENABLE_OOXML_TOOLS env var. tools/list filters them out and tools/call returns method-not-found until the flag is set, so api.ooxml.dev/mcp's existing surface (search_ecma_spec / get_section / list_parts) is unaffected. Tools: ooxml_lookup_element qname (w:tbl, {ns}local, or bare) -> symbol info ooxml_lookup_type qname -> complexType or simpleType symbol ooxml_children element/type/group qname -> ordered child + group ref list ooxml_attributes element/type qname -> attrs unfolded through inheritance and attributeGroup refs ooxml_enum simpleType qname -> enumeration values in declared order ooxml_namespace_info uri -> profiles + symbol counts per profile Query layer (apps/mcp-server/src/ooxml-queries.ts): - parseQName accepts known OOXML prefixes (w/r/s/m/a/wp/pic/c/dgm/xsd), Clark form, or bare local names (defaults to wml-main). - lookupElement / lookupType / lookupSymbolByTypeRef walk xsd_symbol_profiles for profile-scoped hits. - getChildren walks the xsd_inheritance_edges chain via a recursive CTE and unions self + base xsd_child_edges and xsd_group_edges (group refs) in document order. Each entry carries its compositor kind and the type that contributed it. - getAttributes does the same and additionally recurses through attributeGroup refs; each entry carries 'self' / 'inherited' / 'attributeGroup' provenance with the owning name. - getEnums and getNamespaceInfo are direct profile-scoped lookups. Tool dispatch (apps/mcp-server/src/ooxml-tools.ts): - For element qnames passed to ooxml_children / ooxml_attributes the handler looks up the element, follows type_ref to its complexType, then reads from there (per the Phase 4 caveat in PLAN.md). - ooxml_children also falls back to looking up groups by name so users can call it on EG_PContent etc. - Unknown qnames produce a 'Not found' card listing alternative formats and the searched profile. - Default profile is literal 'transitional' until Phase 6. Response shape per PLAN.md: canonical symbol, namespace, type_ref where relevant, source, and a behavior-notes placeholder hooked up to nothing yet (Phase 5 fills it). Tests: 15 query-layer tests against a fresh ingest of the existing fixtures; passes alongside 21 ingest tests for a 36 / 0 total. Worker bundle dry-runs at 263 KiB (67 KiB gzip).
1 parent c742f43 commit e1c5cb0

6 files changed

Lines changed: 1125 additions & 8 deletions

File tree

apps/mcp-server/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { handleMcpRequest } from "./mcp";
1616
export interface Env {
1717
DATABASE_URL: string;
1818
VOYAGE_API_KEY: string;
19+
/**
20+
* Phase 4 feature flag. Set to "true" to expose ooxml_lookup_element /
21+
* ooxml_lookup_type / ooxml_children / ooxml_attributes / ooxml_enum /
22+
* ooxml_namespace_info via tools/list and tools/call. Default off.
23+
*/
24+
ENABLE_OOXML_TOOLS?: string;
1925
}
2026

2127
// Part descriptions

apps/mcp-server/src/mcp.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
import { createDb } from "./db";
88
import { embedQuery } from "./embeddings";
99
import type { Env } from "./index";
10+
import {
11+
OOXML_TOOL_DEFS,
12+
callOoxmlTool,
13+
isOoxmlTool,
14+
ooxmlToolsEnabled,
15+
} from "./ooxml-tools";
1016

1117
// JSON-RPC types
1218
interface JsonRpcRequest {
@@ -132,13 +138,12 @@ function handleInitialize(id: number | string | null): JsonRpcResponse {
132138
};
133139
}
134140

135-
function handleToolsList(id: number | string | null): JsonRpcResponse {
141+
function handleToolsList(id: number | string | null, env: Env): JsonRpcResponse {
142+
const tools = ooxmlToolsEnabled(env) ? [...TOOLS, ...OOXML_TOOL_DEFS] : TOOLS;
136143
return {
137144
jsonrpc: "2.0",
138145
id,
139-
result: {
140-
tools: TOOLS,
141-
},
146+
result: { tools },
142147
};
143148
}
144149

@@ -162,6 +167,25 @@ async function handleToolsCall(
162167
try {
163168
let resultText: string;
164169

170+
// Phase 4 OOXML tools, feature-flagged. tools/list also gates on the same flag,
171+
// so callers should not see these tool names unless the flag is on. Defensive
172+
// check here in case a caller hand-crafts a request.
173+
if (isOoxmlTool(name)) {
174+
if (!ooxmlToolsEnabled(env)) {
175+
return {
176+
jsonrpc: "2.0",
177+
id,
178+
error: { code: METHOD_NOT_FOUND, message: `Unknown tool: ${name}` },
179+
};
180+
}
181+
resultText = await callOoxmlTool(name, args ?? {}, env);
182+
return {
183+
jsonrpc: "2.0",
184+
id,
185+
result: { content: [{ type: "text", text: resultText }] },
186+
};
187+
}
188+
165189
switch (name) {
166190
case "search_ecma_spec": {
167191
const query = args?.query as string;
@@ -374,7 +398,7 @@ export async function handleMcpRequest(request: Request, env: Env): Promise<Resp
374398
return new Response(null, { status: 202 });
375399

376400
case "tools/list":
377-
return jsonResponse(handleToolsList(id));
401+
return jsonResponse(handleToolsList(id, env));
378402

379403
case "tools/call":
380404
return jsonResponse(await handleToolsCall(id, body.params, env));

0 commit comments

Comments
 (0)