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
│ ├── text-splitters/ # Text chunking utilities
│ └── utils/ # Formatting utilities
│ ├── mcp-server.ts # MCP server for external tool integration
│ └── test/ # Manual verification tests
│ └── manual/
│ ├── 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-search-spec-sections "query" # Test vector search tool with query
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
### Imports & Modules
+3
View File
@@ -15,6 +15,8 @@
"test-search-spec-sections": "bun run src/test/manual/test-search-spec-sections.ts",
"agent": "bun run src/agent.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"
},
"keywords": [],
@@ -28,6 +30,7 @@
"@langchain/core": "^0.2.0",
"@langchain/ollama": "^0.1.0",
"@langchain/openai": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.0.4",
"acorn": "^8.16.0",
"cheerio": "^1.2.0",
"glob": "^13.0.6",
+194 -160
View File
@@ -1,19 +1,45 @@
/**
* Evaluate JavaScript code in engine262 and capture spec section marks.
* 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";
// #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.
*/
export const toolMetadata = {
description:
"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.). " +
"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. " +
@@ -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),
});
/**
* 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
interface MarkData {
readonly sectionIds: string[];
@@ -35,12 +93,6 @@ interface MarkData {
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
let engine262Module: any = null;
@@ -56,166 +108,148 @@ async function loadEngine262() {
}
/**
* Creates the evaluateInEngine262 tool.
* Creates the evaluateInEngine262 tool function.
* 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() {
return new DynamicStructuredTool({
name: "ask262_evaluate_in_engine262",
description: toolMetadata.description,
schema: evaluateSchema,
func: async ({ code }) => {
try {
const engine = await loadEngine262();
const ask262Debug = engine.ask262Debug as {
marks: MarkData[];
startTrace: () => void;
stopTrace: () => void;
startImportant: () => void;
stopImportant: () => void;
};
const Agent = engine.Agent;
const ManagedRealm = engine.ManagedRealm;
const setSurroundingAgent = engine.setSurroundingAgent;
const OrdinaryObjectCreate = engine.OrdinaryObjectCreate;
const CreateBuiltinFunction = engine.CreateBuiltinFunction;
const CreateDataProperty = engine.CreateDataProperty;
const Value = engine.Value;
const skipDebugger = engine.skipDebugger;
return async ({ code }: EvaluateToolInput): Promise<EvaluateToolOutput> => {
const engine = await loadEngine262();
const ask262Debug = engine.ask262Debug as {
marks: MarkData[];
startTrace: () => void;
stopTrace: () => void;
startImportant: () => void;
stopImportant: () => void;
};
const Agent = engine.Agent;
const ManagedRealm = engine.ManagedRealm;
const setSurroundingAgent = engine.setSurroundingAgent;
const OrdinaryObjectCreate = engine.OrdinaryObjectCreate;
const CreateBuiltinFunction = engine.CreateBuiltinFunction;
const CreateDataProperty = engine.CreateDataProperty;
const Value = engine.Value;
const skipDebugger = engine.skipDebugger;
// Reset marks from previous runs
ask262Debug.marks = [];
// TODO: Add reset method, allow making instances and use that.
// Reset marks from previous runs
ask262Debug.marks = [];
// Array to capture console output
const consoleOutput: ConsoleEntry[] = [];
// Array to capture console output
const consoleOutput: ConsoleEntry[] = [];
// Set up agent and realm
const agent = new Agent();
setSurroundingAgent(agent);
const realm = new ManagedRealm();
// Set up agent and realm
const agent = new Agent();
setSurroundingAgent(agent);
const realm = new ManagedRealm();
// Expose ask262Debug and console to the evaluated code
realm.scope(() => {
const debugObj = OrdinaryObjectCreate(
agent.intrinsic("%Object.prototype%"),
);
skipDebugger(
CreateDataProperty(
realm.GlobalObject,
Value("ask262Debug"),
debugObj,
),
);
// Expose ask262Debug and console to the evaluated code
realm.scope(() => {
const debugObj = OrdinaryObjectCreate(
agent.intrinsic("%Object.prototype%"),
);
skipDebugger(
CreateDataProperty(realm.GlobalObject, Value("ask262Debug"), debugObj),
);
const startImportant = CreateBuiltinFunction(
() => {
ask262Debug.startImportant();
return Value.undefined;
},
0,
Value("startImportant"),
[],
);
skipDebugger(
CreateDataProperty(
debugObj,
Value("startImportant"),
startImportant,
),
);
const startImportant = CreateBuiltinFunction(
() => {
ask262Debug.startImportant();
// biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
},
0,
Value("startImportant"),
[],
);
skipDebugger(
CreateDataProperty(debugObj, Value("startImportant"), startImportant),
);
const stopImportant = CreateBuiltinFunction(
() => {
ask262Debug.stopImportant();
return Value.undefined;
},
0,
Value("stopImportant"),
[],
);
skipDebugger(
CreateDataProperty(debugObj, Value("stopImportant"), stopImportant),
);
const stopImportant = CreateBuiltinFunction(
() => {
ask262Debug.stopImportant();
// biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
},
0,
Value("stopImportant"),
[],
);
skipDebugger(
CreateDataProperty(debugObj, Value("stopImportant"), stopImportant),
);
// Create console object with methods (excluding 'clear')
const consoleObj = OrdinaryObjectCreate(
agent.intrinsic("%Object.prototype%"),
);
skipDebugger(
CreateDataProperty(
realm.GlobalObject,
Value("console"),
consoleObj,
),
);
// Create console object with methods (excluding 'clear')
const consoleObj = OrdinaryObjectCreate(
agent.intrinsic("%Object.prototype%"),
);
skipDebugger(
CreateDataProperty(realm.GlobalObject, Value("console"), consoleObj),
);
// Add console methods: log, warn, debug, error
const consoleMethods = ["log", "warn", "debug", "error"];
for (const method of consoleMethods) {
const fn = CreateBuiltinFunction(
(args: unknown[]) => {
// Convert engine262 values to JavaScript values for the output
const jsValues = args.map((arg) => {
// Handle engine262 Value types - convert to primitive JS values
if (arg && typeof arg === "object") {
// Try to get string value if it's a JSStringValue
const strVal = (arg as { stringValue?: () => string })
.stringValue;
if (typeof strVal === "function") {
return strVal.call(arg);
}
// Try other common properties
const value = (arg as { value?: unknown }).value;
if (value !== undefined) {
return value;
}
}
return arg;
});
consoleOutput.push({ method, values: jsValues });
return Value.undefined;
},
1,
Value(method),
[],
);
skipDebugger(CreateDataProperty(consoleObj, Value(method), fn));
}
});
// Start tracing
ask262Debug.startTrace();
// Execute the code
realm.evaluateScript(code);
// Stop tracing
ask262Debug.stopTrace();
// Get captured marks
const marks = ask262Debug.marks;
// Filter and group marks by important flag
const importantMarks = marks.filter((m) => m.important);
const otherMarks = marks.filter((m) => !m.important);
// Extract sectionIds, remove fileRelativePath and lineNumber
const result = {
importantSections: importantMarks.map((m) => m.sectionIds),
otherSections: otherMarks.map((m) => m.sectionIds),
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);
// Add console methods: log, warn, debug, error
const consoleMethods = ["log", "warn", "debug", "error"];
for (const method of consoleMethods) {
const fn = CreateBuiltinFunction(
(args: unknown[]) => {
// Convert engine262 values to JavaScript values for the output
const jsValues = args.map((arg) => {
// Handle engine262 Value types - convert to primitive JS values
if (arg && typeof arg === "object") {
// Try to get string value if it's a JSStringValue
const strVal = (arg as { stringValue?: () => string })
.stringValue;
if (typeof strVal === "function") {
return strVal.call(arg);
}
// Try other common properties
const value = (arg as { value?: unknown }).value;
if (value !== undefined) {
return value;
}
}
return arg;
});
consoleOutput.push({ method, values: jsValues });
// biome-ignore lint/suspicious/noExplicitAny: engine262 uses custom value types
return (Value as any)("undefined");
},
1,
Value(method),
[],
);
skipDebugger(CreateDataProperty(consoleObj, Value(method), fn));
}
},
});
});
// Start tracing
ask262Debug.startTrace();
try {
// Execute the code - only this part can fail
realm.evaluateScript(code);
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
};
}
// Stop tracing
ask262Debug.stopTrace();
// Get captured marks
const marks = ask262Debug.marks;
// Filter and group marks by important flag
const importantMarks = marks.filter((m) => m.important);
const otherMarks = marks.filter((m) => !m.important);
// Flatten sectionIds from all marks
return {
importantSections: importantMarks.flatMap((m) => m.sectionIds),
otherSections: otherMarks.flatMap((m) => m.sectionIds),
consoleOutput: consoleOutput,
};
};
}
+82 -49
View File
@@ -4,12 +4,25 @@
*/
import type { Table } from "@lancedb/lancedb";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
/**
* Tool metadata for reuse in OpenCode tools.
*/
// #region Zod schemas (not exported)
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 = {
description:
"Retrieves all text chunks from a specific specification section by sectionid. " +
@@ -24,68 +37,88 @@ export const toolMetadata = {
},
};
const getSectionContentSchema = z.object({
export const inputSchema = z.object({
sectionId: z.string().describe(toolMetadata.args.sectionId),
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.
* Supports recursive fetching - if a section has children, it will fetch all descendants.
* @param table - LanceDB table containing spec vectors
* @returns Function that retrieves content and returns structured output
*/
export function createGetSectionContentTool(table: Table) {
return new DynamicStructuredTool({
name: "ask262_get_section_content",
description: toolMetadata.description,
schema: getSectionContentSchema,
func: async ({ sectionId, recursive }) => {
const allDocs: string[] = [];
const queue: string[] = [sectionId];
const visited = new Set<string>();
return async ({
sectionId,
recursive,
}: GetSectionContentInput): Promise<GetSectionContentOutput> => {
const allDocs: string[] = [];
const queue: string[] = [sectionId];
const visited = new Set<string>();
while (queue.length > 0) {
const currentId = queue.shift()!;
if (visited.has(currentId)) continue;
visited.add(currentId);
while (queue.length > 0) {
const currentId = queue.shift();
if (!currentId || visited.has(currentId)) continue;
visited.add(currentId);
const results = await table
.query()
.where(`sectionid = '${currentId}'`)
.limit(100)
.toArray();
const results = await table
.query()
.where(`sectionid = '${currentId}'`)
.limit(100)
.toArray();
// Sort by partindex to maintain order (nulls last for single-part sections)
const sortedResults = results.sort((a: unknown, b: unknown) => {
const aIndex = (a as { partindex?: number }).partindex ?? Infinity;
const bIndex = (b as { partindex?: number }).partindex ?? Infinity;
return aIndex - bIndex;
});
// Sort by partindex to maintain order (nulls last for single-part sections)
const sortedResults = results.sort((a: unknown, b: unknown) => {
const aIndex = (a as { partindex?: number }).partindex ?? Infinity;
const bIndex = (b as { partindex?: number }).partindex ?? Infinity;
return aIndex - bIndex;
});
for (const result of sortedResults) {
const typedResult = result as {
text?: string;
childrensectionids?: string[];
sectiontitle?: string;
};
for (const result of sortedResults) {
const typedResult = result as {
text?: string;
childrensectionids?: string[];
sectiontitle?: string;
};
if (typedResult.text) {
allDocs.push(typedResult.text);
}
if (typedResult.text) {
allDocs.push(typedResult.text);
}
// Add children to queue for recursive fetching only if recursive is true
if (
recursive &&
typedResult.childrensectionids &&
Array.isArray(typedResult.childrensectionids)
) {
queue.push(...typedResult.childrensectionids);
}
// Add children to queue for recursive fetching only if recursive is true
if (
recursive &&
typedResult.childrensectionids &&
Array.isArray(typedResult.childrensectionids)
) {
queue.push(...typedResult.childrensectionids);
}
}
}
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 {
type ConsoleEntry,
createEvaluateInEngine262Tool,
type EvaluateErrorOutput,
type EvaluateSuccessOutput,
type EvaluateToolInput,
type EvaluateToolOutput,
inputSchema as evaluateInputSchema,
outputSchema as evaluateOutputSchema,
toolMetadata as evaluateToolMetadata,
} from "./evaluateInEngine262";
toolName as evaluateToolName,
} from "./evaluateInEngine262.js";
export {
createGetSectionContentTool,
type GetSectionContentInput,
type GetSectionContentOutput,
inputSchema as getSectionInputSchema,
outputSchema as getSectionOutputSchema,
type SectionContent,
toolMetadata as sectionContentToolMetadata,
} from "./getSectionContent";
export { createGraphExplorerTool } from "./graphExplorer";
export { type RerankResult, rerankDocuments } from "./reranker";
toolName as sectionContentToolName,
} from "./getSectionContent.js";
export { createGraphExplorerTool } from "./graphExplorer.js";
export {
createSearchSpecSectionsTool,
inputSchema as searchSpecInputSchema,
outputSchema as searchSpecOutputSchema,
type SearchSpecInput,
type SearchSpecOutput,
type SearchSpecResult,
toolMetadata as searchSpecToolMetadata,
} from "./searchSpecSections";
toolName as searchSpecToolName,
} from "./searchSpecSections.js";
+53 -24
View File
@@ -4,17 +4,32 @@
*/
import type { Table } from "@lancedb/lancedb";
import { DynamicStructuredTool } from "@langchain/core/tools";
import type { OllamaEmbeddings } from "@langchain/ollama";
import { z } from "zod";
/**
* Tool metadata for reuse in OpenCode tools.
*/
// #region Zod schemas (not exported)
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 = {
description:
"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 " +
"(0-indexed, partIndex+1/totalParts), null if single-part.",
args: {
@@ -23,42 +38,56 @@ export const toolMetadata = {
},
};
const searchSpecSchema = z.object({
export const inputSchema = z.object({
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.
* @param table - LanceDB table containing spec vectors
* @param embeddings - Ollama embeddings instance
* @returns Function that performs the search and returns structured output
*/
export function createSearchSpecSectionsTool(
table: Table,
embeddings: OllamaEmbeddings,
) {
return new DynamicStructuredTool({
name: "ask262_search_spec_sections",
description: toolMetadata.description,
schema: searchSpecSchema,
func: async ({ query }) => {
// Generate embedding for the query
const queryVector = await embeddings.embedQuery(query);
return async ({ query }: SearchSpecInput): Promise<SearchSpecOutput> => {
// Generate embedding for the query
const queryVector = await embeddings.embedQuery(query);
// Search using LanceDB directly, limit to top 5 results
const results = await table.search(queryVector).limit(5).toArray();
// Search using LanceDB directly, limit to top 5 results
const results = await table.search(queryVector).limit(5).toArray();
// Return documents with metadata as JSON
const output = results.map((r: Record<string, unknown>) => ({
// Return documents with metadata as structured objects
const output: SearchSpecResult[] = results.map(
(r: Record<string, unknown>) => ({
sectionId: String(r.sectionid || "unknown"),
sectionTitle: String(r.sectiontitle || "unknown"),
score: Number(r._distance || 0),
partIndex: r.partindex ?? null,
totalParts: r.totalparts ?? null,
partIndex: (r.partindex as number | undefined) ?? null,
totalParts: (r.totalparts as number | undefined) ?? null,
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();