feat(mcp-server): Add MCP server with tool registrations

Add MCP server implementation with support for:
- ask262_search_spec_sections tool
- ask262_get_section_content tool
- ask262_evaluate_in_engine262 tool
Includes proper error handling, input/output schemas, and structured
content responses.
This commit is contained in:
2026-04-14 11:12:03 +05:30
parent 1c9d08f7fa
commit 887ad3e5a4
8 changed files with 742 additions and 238 deletions
+65
View File
@@ -38,6 +38,7 @@ ask262/
│ ├── htmlAddInternalMethodLink.ts # Adds internal method links │ ├── htmlAddInternalMethodLink.ts # Adds internal method links
│ ├── text-splitters/ # Text chunking utilities │ ├── text-splitters/ # Text chunking utilities
│ └── utils/ # Formatting utilities │ └── utils/ # Formatting utilities
│ ├── mcp-server.ts # MCP server for external tool integration
│ └── test/ # Manual verification tests │ └── test/ # Manual verification tests
│ └── manual/ │ └── manual/
│ ├── verify-db.ts # Verify database contents │ ├── verify-db.ts # Verify database contents
@@ -66,8 +67,72 @@ bun test # Run all tests
bun run test-evaluate # Test evaluate in engine262 tool bun run test-evaluate # Test evaluate in engine262 tool
bun run test-search-spec-sections "query" # Test vector search tool with query bun run test-search-spec-sections "query" # Test vector search tool with query
bun run agent "Query" # Run agent with question bun run agent "Query" # Run agent with question
bun run mcp-server # Start MCP server (stdio transport)
bun run test-mcp-server # Test MCP server with all tools
``` ```
## MCP Server
Ask262 can run as an MCP (Model Context Protocol) server, making its tools available to any MCP-compatible client (Claude Desktop, OpenCode, etc.).
### Available MCP Tools
| Tool | Description |
|------|-------------|
| `ask262_search_spec_sections` | Vector search ECMAScript spec for relevant sections |
| `ask262_get_section_content` | Retrieve full content from a spec section |
| `ask262_evaluate_in_engine262` | Execute JS in engine262 and capture spec sections |
### Configuration
Add to your MCP client configuration:
**Claude Desktop (`claude_desktop_config.json`):**
```json
{
"mcpServers": {
"ask262": {
"command": "bun",
"args": ["run", "/path/to/ask262/src/mcp-server.ts"],
"cwd": "/path/to/ask262"
}
}
}
```
**OpenCode (`.opencode/mcp.json`):**
```json
{
"servers": {
"ask262": {
"command": "bun",
"args": ["run", "src/mcp-server.ts"]
}
}
}
```
### Testing
Test the MCP server before configuring your client:
```bash
# Run automated tests for all MCP tools
bun run test-mcp-server
```
This tests:
- Tool listing
- Vector search (`ask262_search_spec_sections`)
- Section content retrieval (`ask262_get_section_content`)
- Code evaluation with console capture (`ask262_evaluate_in_engine262`)
### Prerequisites
Before running the MCP server:
1. Ensure `storage/` directory exists with ingested spec vectors (`bun run ingest`)
2. Ensure Ollama is running with `qwen3-embedding:0.6b` model
## Code Style Guidelines ## Code Style Guidelines
### Imports & Modules ### Imports & Modules
+3
View File
@@ -15,6 +15,8 @@
"test-search-spec-sections": "bun run src/test/manual/test-search-spec-sections.ts", "test-search-spec-sections": "bun run src/test/manual/test-search-spec-sections.ts",
"agent": "bun run src/agent.ts", "agent": "bun run src/agent.ts",
"build": "bun run src/setup/buildGraph.ts", "build": "bun run src/setup/buildGraph.ts",
"mcp-server": "bun run src/mcp-server.ts",
"test-mcp-server": "bun run src/test/manual/test-mcp-server.ts",
"test": "bun test" "test": "bun test"
}, },
"keywords": [], "keywords": [],
@@ -28,6 +30,7 @@
"@langchain/core": "^0.2.0", "@langchain/core": "^0.2.0",
"@langchain/ollama": "^0.1.0", "@langchain/ollama": "^0.1.0",
"@langchain/openai": "^0.1.0", "@langchain/openai": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.0.4",
"acorn": "^8.16.0", "acorn": "^8.16.0",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"glob": "^13.0.6", "glob": "^13.0.6",
+86 -52
View File
@@ -1,19 +1,45 @@
/** /**
* Evaluate JavaScript code in engine262 and capture spec section marks. * Evaluate JavaScript code in engine262 and capture spec section marks.
* Executes code in the engine262 JavaScript engine and returns the captured * Executes code in the engine262 JavaScript engine and returns the captured
* ECMAScript spec section marks as JSON. * ECMAScript spec section marks.
*/ */
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod"; import { z } from "zod";
// #region Zod schemas (not exported)
const consoleEntrySchema = z.object({
method: z.string().describe("Console method name (log, warn, debug, error)"),
values: z.array(z.unknown()).describe("Values logged to console"),
});
const evaluateSuccessOutputSchema = z.object({
importantSections: z
.array(z.string())
.describe("Important spec sections hit during execution"),
otherSections: z
.array(z.string())
.describe("Other spec sections hit during execution"),
consoleOutput: z
.array(consoleEntrySchema)
.describe("Console output captured during execution"),
});
const evaluateErrorOutputSchema = z.object({
error: z.string().describe("Error message when execution fails"),
});
// #endregion
// #region Exported Zod schemas
/** /**
* Tool metadata for reuse in OpenCode tools. * Tool metadata for reuse in OpenCode tools.
*/ */
export const toolMetadata = { export const toolMetadata = {
description: description:
"Executes pure ECMAScript JavaScript code in the engine262 JavaScript engine and captures which ECMAScript specification sections are hit during execution. " + "Executes pure ECMAScript JavaScript code in the engine262 JavaScript engine and captures which ECMAScript specification sections are hit during execution. " +
"Returns JSON with importantSections, otherSections, and consoleOutput arrays. Useful for understanding how specific JavaScript operations map to the ECMAScript spec. " + "Returns an object with importantSections, otherSections, and consoleOutput arrays. Useful for understanding how specific JavaScript operations map to the ECMAScript spec. " +
"Code must be pure ECMAScript with no DOM, browser, or Node.js APIs (no fs, document, window, etc.). " + "Code must be pure ECMAScript with no DOM, browser, or Node.js APIs (no fs, document, window, etc.). " +
"console object with log/warn/debug/error methods and ask262Debug are available globally (no import needed). " + "console object with log/warn/debug/error methods and ask262Debug are available globally (no import needed). " +
"Use ask262Debug.startImportant() and ask262Debug.stopImportant() to mark important sections. " + "Use ask262Debug.startImportant() and ask262Debug.stopImportant() to mark important sections. " +
@@ -23,10 +49,42 @@ export const toolMetadata = {
}, },
}; };
const evaluateSchema = z.object({ /**
* Input schema for the evaluate tool.
*/
export const inputSchema = z.object({
code: z.string().describe(toolMetadata.args.code), code: z.string().describe(toolMetadata.args.code),
}); });
/**
* Output schema for the evaluate tool (union of success and error outputs).
*/
export const outputSchema = z.union([
evaluateSuccessOutputSchema,
evaluateErrorOutputSchema,
]);
// #endregion
// #region TypeScript types (inferred from Zod schemas)
export type ConsoleEntry = z.infer<typeof consoleEntrySchema>;
export type EvaluateSuccessOutput = z.infer<typeof evaluateSuccessOutputSchema>;
export type EvaluateErrorOutput = z.infer<typeof evaluateErrorOutputSchema>;
export type EvaluateToolOutput = z.infer<typeof outputSchema>;
export type EvaluateToolInput = z.infer<typeof inputSchema>;
// #endregion
/**
* Tool name constant.
*/
export const toolName = "ask262_evaluate_in_engine262";
// Type definitions for engine262 module // Type definitions for engine262 module
interface MarkData { interface MarkData {
readonly sectionIds: string[]; readonly sectionIds: string[];
@@ -35,12 +93,6 @@ interface MarkData {
readonly important: boolean; readonly important: boolean;
} }
// Console log entry type
interface ConsoleEntry {
method: string;
values: unknown[];
}
// biome-ignore lint/suspicious/noExplicitAny: engine262 is an external module without TypeScript types // biome-ignore lint/suspicious/noExplicitAny: engine262 is an external module without TypeScript types
let engine262Module: any = null; let engine262Module: any = null;
@@ -56,17 +108,12 @@ async function loadEngine262() {
} }
/** /**
* Creates the evaluateInEngine262 tool. * Creates the evaluateInEngine262 tool function.
* Executes JavaScript code in engine262 and captures spec section marks. * Executes JavaScript code in engine262 and captures spec section marks.
* @returns The evaluate_in_engine262 tool instance * @returns Function that executes code and returns structured output
*/ */
export function createEvaluateInEngine262Tool() { export function createEvaluateInEngine262Tool() {
return new DynamicStructuredTool({ return async ({ code }: EvaluateToolInput): Promise<EvaluateToolOutput> => {
name: "ask262_evaluate_in_engine262",
description: toolMetadata.description,
schema: evaluateSchema,
func: async ({ code }) => {
try {
const engine = await loadEngine262(); const engine = await loadEngine262();
const ask262Debug = engine.ask262Debug as { const ask262Debug = engine.ask262Debug as {
marks: MarkData[]; marks: MarkData[];
@@ -84,6 +131,7 @@ export function createEvaluateInEngine262Tool() {
const Value = engine.Value; const Value = engine.Value;
const skipDebugger = engine.skipDebugger; const skipDebugger = engine.skipDebugger;
// TODO: Add reset method, allow making instances and use that.
// Reset marks from previous runs // Reset marks from previous runs
ask262Debug.marks = []; ask262Debug.marks = [];
@@ -101,34 +149,28 @@ export function createEvaluateInEngine262Tool() {
agent.intrinsic("%Object.prototype%"), agent.intrinsic("%Object.prototype%"),
); );
skipDebugger( skipDebugger(
CreateDataProperty( CreateDataProperty(realm.GlobalObject, Value("ask262Debug"), debugObj),
realm.GlobalObject,
Value("ask262Debug"),
debugObj,
),
); );
const startImportant = CreateBuiltinFunction( const startImportant = CreateBuiltinFunction(
() => { () => {
ask262Debug.startImportant(); ask262Debug.startImportant();
return Value.undefined; // biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
}, },
0, 0,
Value("startImportant"), Value("startImportant"),
[], [],
); );
skipDebugger( skipDebugger(
CreateDataProperty( CreateDataProperty(debugObj, Value("startImportant"), startImportant),
debugObj,
Value("startImportant"),
startImportant,
),
); );
const stopImportant = CreateBuiltinFunction( const stopImportant = CreateBuiltinFunction(
() => { () => {
ask262Debug.stopImportant(); ask262Debug.stopImportant();
return Value.undefined; // biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
}, },
0, 0,
Value("stopImportant"), Value("stopImportant"),
@@ -143,11 +185,7 @@ export function createEvaluateInEngine262Tool() {
agent.intrinsic("%Object.prototype%"), agent.intrinsic("%Object.prototype%"),
); );
skipDebugger( skipDebugger(
CreateDataProperty( CreateDataProperty(realm.GlobalObject, Value("console"), consoleObj),
realm.GlobalObject,
Value("console"),
consoleObj,
),
); );
// Add console methods: log, warn, debug, error // Add console methods: log, warn, debug, error
@@ -174,7 +212,8 @@ export function createEvaluateInEngine262Tool() {
return arg; return arg;
}); });
consoleOutput.push({ method, values: jsValues }); consoleOutput.push({ method, values: jsValues });
return Value.undefined; // biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
}, },
1, 1,
Value(method), Value(method),
@@ -187,8 +226,14 @@ export function createEvaluateInEngine262Tool() {
// Start tracing // Start tracing
ask262Debug.startTrace(); ask262Debug.startTrace();
// Execute the code try {
// Execute the code - only this part can fail
realm.evaluateScript(code); realm.evaluateScript(code);
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
};
}
// Stop tracing // Stop tracing
ask262Debug.stopTrace(); ask262Debug.stopTrace();
@@ -200,22 +245,11 @@ export function createEvaluateInEngine262Tool() {
const importantMarks = marks.filter((m) => m.important); const importantMarks = marks.filter((m) => m.important);
const otherMarks = marks.filter((m) => !m.important); const otherMarks = marks.filter((m) => !m.important);
// Extract sectionIds, remove fileRelativePath and lineNumber // Flatten sectionIds from all marks
const result = { return {
importantSections: importantMarks.map((m) => m.sectionIds), importantSections: importantMarks.flatMap((m) => m.sectionIds),
otherSections: otherMarks.map((m) => m.sectionIds), otherSections: otherMarks.flatMap((m) => m.sectionIds),
consoleOutput: consoleOutput, consoleOutput: consoleOutput,
}; };
// Return compressed JSON
return JSON.stringify(result);
} catch (error) {
console.error(`[Tool: ask262_evaluate_in_engine262] Error: ${error}`);
const errorResult = {
error: error instanceof Error ? error.message : String(error),
}; };
return JSON.stringify(errorResult);
}
},
});
} }
+49 -16
View File
@@ -4,12 +4,25 @@
*/ */
import type { Table } from "@lancedb/lancedb"; import type { Table } from "@lancedb/lancedb";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod"; import { z } from "zod";
/** // #region Zod schemas (not exported)
* Tool metadata for reuse in OpenCode tools.
*/ const sectionContentSchema = z.object({
text: z.string(),
sectionTitle: z.string().optional(),
partIndex: z.number().optional(),
});
const getSectionContentOutputSchema = z.object({
content: z.string(),
sectionCount: z.number(),
});
// #endregion
// #region Exported Zod schemas
export const toolMetadata = { export const toolMetadata = {
description: description:
"Retrieves all text chunks from a specific specification section by sectionid. " + "Retrieves all text chunks from a specific specification section by sectionid. " +
@@ -24,30 +37,48 @@ export const toolMetadata = {
}, },
}; };
const getSectionContentSchema = z.object({ export const inputSchema = z.object({
sectionId: z.string().describe(toolMetadata.args.sectionId), sectionId: z.string().describe(toolMetadata.args.sectionId),
recursive: z.boolean().default(true).describe(toolMetadata.args.recursive), recursive: z.boolean().default(true).describe(toolMetadata.args.recursive),
}); });
export const outputSchema = getSectionContentOutputSchema;
export const toolName = "ask262_get_section_content";
// #endregion
// #region TypeScript types (inferred from Zod schemas)
export type SectionContent = z.infer<typeof sectionContentSchema>;
export type GetSectionContentOutput = z.infer<
typeof getSectionContentOutputSchema
>;
export type GetSectionContentInput = z.infer<typeof inputSchema>;
// #endregion
/** /**
* Creates the get section content tool. * Creates the get section content tool function.
* Retrieves all text chunks from a specific specification section by sectionid. * Retrieves all text chunks from a specific specification section by sectionid.
* Supports recursive fetching - if a section has children, it will fetch all descendants. * Supports recursive fetching - if a section has children, it will fetch all descendants.
* @param table - LanceDB table containing spec vectors * @param table - LanceDB table containing spec vectors
* @returns Function that retrieves content and returns structured output
*/ */
export function createGetSectionContentTool(table: Table) { export function createGetSectionContentTool(table: Table) {
return new DynamicStructuredTool({ return async ({
name: "ask262_get_section_content", sectionId,
description: toolMetadata.description, recursive,
schema: getSectionContentSchema, }: GetSectionContentInput): Promise<GetSectionContentOutput> => {
func: async ({ sectionId, recursive }) => {
const allDocs: string[] = []; const allDocs: string[] = [];
const queue: string[] = [sectionId]; const queue: string[] = [sectionId];
const visited = new Set<string>(); const visited = new Set<string>();
while (queue.length > 0) { while (queue.length > 0) {
const currentId = queue.shift()!; const currentId = queue.shift();
if (visited.has(currentId)) continue; if (!currentId || visited.has(currentId)) continue;
visited.add(currentId); visited.add(currentId);
const results = await table const results = await table
@@ -85,7 +116,9 @@ export function createGetSectionContentTool(table: Table) {
} }
} }
return allDocs.join("\n\n---\n\n"); return {
}, content: allDocs.join("\n\n---\n\n"),
}); sectionCount: visited.size,
};
};
} }
+24 -5
View File
@@ -4,16 +4,35 @@
*/ */
export { export {
type ConsoleEntry,
createEvaluateInEngine262Tool, createEvaluateInEngine262Tool,
type EvaluateErrorOutput,
type EvaluateSuccessOutput,
type EvaluateToolInput,
type EvaluateToolOutput,
inputSchema as evaluateInputSchema,
outputSchema as evaluateOutputSchema,
toolMetadata as evaluateToolMetadata, toolMetadata as evaluateToolMetadata,
} from "./evaluateInEngine262"; toolName as evaluateToolName,
} from "./evaluateInEngine262.js";
export { export {
createGetSectionContentTool, createGetSectionContentTool,
type GetSectionContentInput,
type GetSectionContentOutput,
inputSchema as getSectionInputSchema,
outputSchema as getSectionOutputSchema,
type SectionContent,
toolMetadata as sectionContentToolMetadata, toolMetadata as sectionContentToolMetadata,
} from "./getSectionContent"; toolName as sectionContentToolName,
export { createGraphExplorerTool } from "./graphExplorer"; } from "./getSectionContent.js";
export { type RerankResult, rerankDocuments } from "./reranker"; export { createGraphExplorerTool } from "./graphExplorer.js";
export { export {
createSearchSpecSectionsTool, createSearchSpecSectionsTool,
inputSchema as searchSpecInputSchema,
outputSchema as searchSpecOutputSchema,
type SearchSpecInput,
type SearchSpecOutput,
type SearchSpecResult,
toolMetadata as searchSpecToolMetadata, toolMetadata as searchSpecToolMetadata,
} from "./searchSpecSections"; toolName as searchSpecToolName,
} from "./searchSpecSections.js";
+49 -20
View File
@@ -4,17 +4,32 @@
*/ */
import type { Table } from "@lancedb/lancedb"; import type { Table } from "@lancedb/lancedb";
import { DynamicStructuredTool } from "@langchain/core/tools";
import type { OllamaEmbeddings } from "@langchain/ollama"; import type { OllamaEmbeddings } from "@langchain/ollama";
import { z } from "zod"; import { z } from "zod";
/** // #region Zod schemas (not exported)
* Tool metadata for reuse in OpenCode tools.
*/ const searchSpecResultSchema = z.object({
sectionId: z.string(),
sectionTitle: z.string(),
score: z.number(),
partIndex: z.number().nullable(),
totalParts: z.number().nullable(),
content: z.string(),
});
const searchSpecOutputSchema = z.object({
results: z.array(searchSpecResultSchema),
});
// #endregion
// #region Exported Zod schemas
export const toolMetadata = { export const toolMetadata = {
description: description:
"Vector search the ECMAScript specification for sections relevant to a query. " + "Vector search the ECMAScript specification for sections relevant to a query. " +
"Returns JSON array with sectionId, sectionTitle, score, partIndex, totalParts, and content. " + "Returns an array of sections with sectionId, sectionTitle, score, partIndex, totalParts, and content. " +
"partIndex and totalParts indicate which chunk of a multi-part section this is " + "partIndex and totalParts indicate which chunk of a multi-part section this is " +
"(0-indexed, partIndex+1/totalParts), null if single-part.", "(0-indexed, partIndex+1/totalParts), null if single-part.",
args: { args: {
@@ -23,42 +38,56 @@ export const toolMetadata = {
}, },
}; };
const searchSpecSchema = z.object({ export const inputSchema = z.object({
query: z.string().describe(toolMetadata.args.query), query: z.string().describe(toolMetadata.args.query),
}); });
export const outputSchema = searchSpecOutputSchema;
export const toolName = "ask262_search_spec_sections";
// #endregion
// #region TypeScript types (inferred from Zod schemas)
export type SearchSpecResult = z.infer<typeof searchSpecResultSchema>;
export type SearchSpecOutput = z.infer<typeof searchSpecOutputSchema>;
export type SearchSpecInput = z.infer<typeof inputSchema>;
// #endregion
/** /**
* Creates the search spec sections tool. * Creates the search spec sections tool function.
* Performs semantic vector search to find relevant spec sections. * Performs semantic vector search to find relevant spec sections.
* @param table - LanceDB table containing spec vectors * @param table - LanceDB table containing spec vectors
* @param embeddings - Ollama embeddings instance * @param embeddings - Ollama embeddings instance
* @returns Function that performs the search and returns structured output
*/ */
export function createSearchSpecSectionsTool( export function createSearchSpecSectionsTool(
table: Table, table: Table,
embeddings: OllamaEmbeddings, embeddings: OllamaEmbeddings,
) { ) {
return new DynamicStructuredTool({ return async ({ query }: SearchSpecInput): Promise<SearchSpecOutput> => {
name: "ask262_search_spec_sections",
description: toolMetadata.description,
schema: searchSpecSchema,
func: async ({ query }) => {
// Generate embedding for the query // Generate embedding for the query
const queryVector = await embeddings.embedQuery(query); const queryVector = await embeddings.embedQuery(query);
// Search using LanceDB directly, limit to top 5 results // Search using LanceDB directly, limit to top 5 results
const results = await table.search(queryVector).limit(5).toArray(); const results = await table.search(queryVector).limit(5).toArray();
// Return documents with metadata as JSON // Return documents with metadata as structured objects
const output = results.map((r: Record<string, unknown>) => ({ const output: SearchSpecResult[] = results.map(
(r: Record<string, unknown>) => ({
sectionId: String(r.sectionid || "unknown"), sectionId: String(r.sectionid || "unknown"),
sectionTitle: String(r.sectiontitle || "unknown"), sectionTitle: String(r.sectiontitle || "unknown"),
score: Number(r._distance || 0), score: Number(r._distance || 0),
partIndex: r.partindex ?? null, partIndex: (r.partindex as number | undefined) ?? null,
totalParts: r.totalparts ?? null, totalParts: (r.totalparts as number | undefined) ?? null,
content: String(r.text || ""), content: String(r.text || ""),
})); }),
);
return JSON.stringify(output); return { results: output };
}, };
});
} }
+182
View File
@@ -0,0 +1,182 @@
/**
* Ask262 MCP Server
* Provides MCP-compatible tools for exploring the ECMAScript specification.
*/
import * as lancedbSdk from "@lancedb/lancedb";
import { OllamaEmbeddings } from "@langchain/ollama";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
createEvaluateInEngine262Tool,
createGetSectionContentTool,
createSearchSpecSectionsTool,
type EvaluateToolInput,
type EvaluateToolOutput,
evaluateInputSchema,
evaluateOutputSchema,
evaluateToolMetadata,
evaluateToolName,
type GetSectionContentInput,
type GetSectionContentOutput,
getSectionInputSchema,
getSectionOutputSchema,
type SearchSpecInput,
type SearchSpecOutput,
searchSpecInputSchema,
searchSpecOutputSchema,
searchSpecToolMetadata,
searchSpecToolName,
sectionContentToolMetadata,
sectionContentToolName,
} from "./agent-tools/index.js";
import { STORAGE_DIR } from "./constants.js";
// #region MCP Types
/**
* MCP text content item for tool responses.
*/
export interface McpTextContent {
type: "text";
text: string;
}
// Base MCP output type with index signature for SDK compatibility
interface McpToolOutputBase {
[key: string]: unknown;
content: McpTextContent[];
}
// Evaluate tool MCP types
export type EvaluateToolMCPInput = EvaluateToolInput;
export interface EvaluateToolMCPOutput extends McpToolOutputBase {
structuredContent: EvaluateToolOutput;
isError?: boolean;
}
// Get section content tool MCP types
export type GetSectionContentMCPInput = GetSectionContentInput;
export interface GetSectionContentMCPOutput extends McpToolOutputBase {
structuredContent: GetSectionContentOutput;
isError?: boolean;
}
// Search spec tool MCP types
export type SearchSpecMCPInput = SearchSpecInput;
export interface SearchSpecMCPOutput extends McpToolOutputBase {
structuredContent: SearchSpecOutput;
isError?: boolean;
}
// #endregion
// Initialize embeddings
const embeddings = new OllamaEmbeddings({
model: "qwen3-embedding:0.6b",
});
async function main() {
// Connect to LanceDB
const db = await lancedbSdk.connect(STORAGE_DIR);
const table = await db.openTable("spec_vectors");
// Create tool instances
const searchSpecTool = createSearchSpecSectionsTool(table, embeddings);
const getSectionContentTool = createGetSectionContentTool(table);
const evaluateTool = createEvaluateInEngine262Tool();
// Create MCP server
const server = new McpServer({
name: "ask262-server",
version: "1.0.0",
});
// Register search spec tool
server.registerTool(
searchSpecToolName,
{
description: searchSpecToolMetadata.description,
inputSchema: searchSpecInputSchema,
outputSchema: searchSpecOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
async ({ query }: SearchSpecMCPInput): Promise<SearchSpecMCPOutput> => {
const result = await searchSpecTool({ query });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
);
// Register get section content tool
server.registerTool(
sectionContentToolName,
{
description: sectionContentToolMetadata.description,
inputSchema: getSectionInputSchema,
outputSchema: getSectionOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
async ({
sectionId,
recursive,
}: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => {
const result = await getSectionContentTool({ sectionId, recursive });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
);
// Register evaluate in engine262 tool
server.registerTool(
evaluateToolName,
{
description: evaluateToolMetadata.description,
inputSchema: evaluateInputSchema,
outputSchema: evaluateOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: false,
openWorldHint: false,
},
},
async ({ code }: EvaluateToolMCPInput): Promise<EvaluateToolMCPOutput> => {
const result = await evaluateTool({ code });
const isError = "error" in result;
const text = isError ? result.error : JSON.stringify(result, null, 2);
return {
content: [{ type: "text", text }],
structuredContent: result,
isError,
};
},
);
// Use stdio transport for communication
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Ask262 MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
+139
View File
@@ -0,0 +1,139 @@
/**
* Test script for Ask262 MCP Server
* Tests all available tools
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import type {
EvaluateToolMCPOutput,
GetSectionContentMCPOutput,
SearchSpecMCPOutput,
} from "../../mcp-server.js";
async function testMCPServer() {
console.log("Starting MCP Server tests...\n");
// Create transport connecting to the server
const transport = new StdioClientTransport({
command: "bun",
args: ["run", "src/mcp-server.ts"],
});
// Create client
const client = new Client(
{
name: "test-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
try {
// Connect to server
await client.connect(transport);
console.log("✓ Connected to MCP server\n");
// Test 1: List tools
console.log("Test 1: Listing available tools...");
const tools = await client.listTools();
console.log(`Found ${tools.tools.length} tools:`);
for (const tool of tools.tools) {
const desc = tool.description ?? "No description";
console.log(` - ${tool.name}: ${desc.substring(0, 60)}...`);
}
console.log("✓ Tools listed successfully\n");
// Test 2: Search spec sections
console.log("Test 2: Testing ask262_search_spec_sections...");
const searchResult = (await client.callTool({
name: "ask262_search_spec_sections",
arguments: {
query: "array map method",
},
})) as SearchSpecMCPOutput;
if (searchResult.isError) {
throw new Error(`Search failed: ${searchResult.content[0]?.text}`);
}
const searchData = searchResult.structuredContent;
console.log(`Found ${searchData.results?.length ?? 0} sections`);
if (searchData.results && searchData.results.length > 0) {
console.log(
` First result: ${searchData.results[0].sectionId} - ${searchData.results[0].sectionTitle}`,
);
}
console.log("✓ Search working (isError: false)\n");
// Test 3: Get section content
console.log("Test 3: Testing ask262_get_section_content...");
const contentResult = (await client.callTool({
name: "ask262_get_section_content",
arguments: {
sectionId: "sec-array-prototype-map",
recursive: false,
},
})) as GetSectionContentMCPOutput;
if (contentResult.isError) {
throw new Error(`Get section failed: ${contentResult.content[0]?.text}`);
}
const contentData = contentResult.structuredContent;
console.log(
`Content length: ${contentData.content?.length ?? 0} characters`,
);
console.log(`Sections visited: ${contentData.sectionCount ?? 0}`);
console.log("✓ Section content retrieved (isError: false)\n");
// Test 4: Evaluate in engine262 - success case
console.log("Test 4: Testing ask262_evaluate_in_engine262 (success)...");
const evalResult = (await client.callTool({
name: "ask262_evaluate_in_engine262",
arguments: {
code: "console.log('test'); let x = 1 + 2; console.log(x);",
},
})) as EvaluateToolMCPOutput;
if (evalResult.isError) {
throw new Error(`Evaluate failed: ${evalResult.content[0]?.text}`);
}
// Type guard: check for error in structuredContent
const evalData = evalResult.structuredContent;
if ("error" in evalData) {
throw new Error(`Unexpected error: ${evalData.error}`);
}
console.log(
`Important sections: ${evalData.importantSections?.length ?? 0}`,
);
console.log(`Other sections: ${evalData.otherSections?.length ?? 0}`);
console.log(`Console output: ${JSON.stringify(evalData.consoleOutput)}`);
console.log("✓ Code evaluation working (isError: false)\n");
// Test 5: Evaluate in engine262 - error case
console.log("Test 5: Testing ask262_evaluate_in_engine262 (error)...");
const evalErrorResult = (await client.callTool({
name: "ask262_evaluate_in_engine262",
arguments: {
code: "invalid syntax here @#$%",
},
})) as EvaluateToolMCPOutput;
if (!evalErrorResult.isError) {
throw new Error("Expected error but got success");
}
// Verify error is in structuredContent
const errorData = evalErrorResult.structuredContent;
if (!("error" in errorData)) {
throw new Error("Expected error in structuredContent");
}
console.log(`Got expected error: ${errorData.error}`);
console.log("✓ Error handling working (isError: true)\n");
console.log("All tests passed! ✓");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
} finally {
await client.close();
}
}
testMCPServer();