mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,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.
|
* 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",
|
const engine = await loadEngine262();
|
||||||
description: toolMetadata.description,
|
const ask262Debug = engine.ask262Debug as {
|
||||||
schema: evaluateSchema,
|
marks: MarkData[];
|
||||||
func: async ({ code }) => {
|
startTrace: () => void;
|
||||||
try {
|
stopTrace: () => void;
|
||||||
const engine = await loadEngine262();
|
startImportant: () => void;
|
||||||
const ask262Debug = engine.ask262Debug as {
|
stopImportant: () => void;
|
||||||
marks: MarkData[];
|
};
|
||||||
startTrace: () => void;
|
const Agent = engine.Agent;
|
||||||
stopTrace: () => void;
|
const ManagedRealm = engine.ManagedRealm;
|
||||||
startImportant: () => void;
|
const setSurroundingAgent = engine.setSurroundingAgent;
|
||||||
stopImportant: () => void;
|
const OrdinaryObjectCreate = engine.OrdinaryObjectCreate;
|
||||||
};
|
const CreateBuiltinFunction = engine.CreateBuiltinFunction;
|
||||||
const Agent = engine.Agent;
|
const CreateDataProperty = engine.CreateDataProperty;
|
||||||
const ManagedRealm = engine.ManagedRealm;
|
const Value = engine.Value;
|
||||||
const setSurroundingAgent = engine.setSurroundingAgent;
|
const skipDebugger = engine.skipDebugger;
|
||||||
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
|
// TODO: Add reset method, allow making instances and use that.
|
||||||
ask262Debug.marks = [];
|
// Reset marks from previous runs
|
||||||
|
ask262Debug.marks = [];
|
||||||
|
|
||||||
// Array to capture console output
|
// Array to capture console output
|
||||||
const consoleOutput: ConsoleEntry[] = [];
|
const consoleOutput: ConsoleEntry[] = [];
|
||||||
|
|
||||||
// Set up agent and realm
|
// Set up agent and realm
|
||||||
const agent = new Agent();
|
const agent = new Agent();
|
||||||
setSurroundingAgent(agent);
|
setSurroundingAgent(agent);
|
||||||
const realm = new ManagedRealm();
|
const realm = new ManagedRealm();
|
||||||
|
|
||||||
// Expose ask262Debug and console to the evaluated code
|
// Expose ask262Debug and console to the evaluated code
|
||||||
realm.scope(() => {
|
realm.scope(() => {
|
||||||
const debugObj = OrdinaryObjectCreate(
|
const debugObj = OrdinaryObjectCreate(
|
||||||
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,
|
},
|
||||||
Value("startImportant"),
|
0,
|
||||||
[],
|
Value("startImportant"),
|
||||||
);
|
[],
|
||||||
skipDebugger(
|
);
|
||||||
CreateDataProperty(
|
skipDebugger(
|
||||||
debugObj,
|
CreateDataProperty(debugObj, Value("startImportant"), startImportant),
|
||||||
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,
|
},
|
||||||
Value("stopImportant"),
|
0,
|
||||||
[],
|
Value("stopImportant"),
|
||||||
);
|
[],
|
||||||
skipDebugger(
|
);
|
||||||
CreateDataProperty(debugObj, Value("stopImportant"), stopImportant),
|
skipDebugger(
|
||||||
);
|
CreateDataProperty(debugObj, Value("stopImportant"), stopImportant),
|
||||||
|
);
|
||||||
|
|
||||||
// Create console object with methods (excluding 'clear')
|
// Create console object with methods (excluding 'clear')
|
||||||
const consoleObj = OrdinaryObjectCreate(
|
const consoleObj = OrdinaryObjectCreate(
|
||||||
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
|
||||||
const consoleMethods = ["log", "warn", "debug", "error"];
|
const consoleMethods = ["log", "warn", "debug", "error"];
|
||||||
for (const method of consoleMethods) {
|
for (const method of consoleMethods) {
|
||||||
const fn = CreateBuiltinFunction(
|
const fn = CreateBuiltinFunction(
|
||||||
(args: unknown[]) => {
|
(args: unknown[]) => {
|
||||||
// Convert engine262 values to JavaScript values for the output
|
// Convert engine262 values to JavaScript values for the output
|
||||||
const jsValues = args.map((arg) => {
|
const jsValues = args.map((arg) => {
|
||||||
// Handle engine262 Value types - convert to primitive JS values
|
// Handle engine262 Value types - convert to primitive JS values
|
||||||
if (arg && typeof arg === "object") {
|
if (arg && typeof arg === "object") {
|
||||||
// Try to get string value if it's a JSStringValue
|
// Try to get string value if it's a JSStringValue
|
||||||
const strVal = (arg as { stringValue?: () => string })
|
const strVal = (arg as { stringValue?: () => string })
|
||||||
.stringValue;
|
.stringValue;
|
||||||
if (typeof strVal === "function") {
|
if (typeof strVal === "function") {
|
||||||
return strVal.call(arg);
|
return strVal.call(arg);
|
||||||
}
|
}
|
||||||
// Try other common properties
|
// Try other common properties
|
||||||
const value = (arg as { value?: unknown }).value;
|
const value = (arg as { value?: unknown }).value;
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,
|
},
|
||||||
Value(method),
|
1,
|
||||||
[],
|
Value(method),
|
||||||
);
|
[],
|
||||||
skipDebugger(CreateDataProperty(consoleObj, Value(method), fn));
|
);
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,68 +37,88 @@ 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
|
||||||
.query()
|
.query()
|
||||||
.where(`sectionid = '${currentId}'`)
|
.where(`sectionid = '${currentId}'`)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
// Sort by partindex to maintain order (nulls last for single-part sections)
|
// Sort by partindex to maintain order (nulls last for single-part sections)
|
||||||
const sortedResults = results.sort((a: unknown, b: unknown) => {
|
const sortedResults = results.sort((a: unknown, b: unknown) => {
|
||||||
const aIndex = (a as { partindex?: number }).partindex ?? Infinity;
|
const aIndex = (a as { partindex?: number }).partindex ?? Infinity;
|
||||||
const bIndex = (b as { partindex?: number }).partindex ?? Infinity;
|
const bIndex = (b as { partindex?: number }).partindex ?? Infinity;
|
||||||
return aIndex - bIndex;
|
return aIndex - bIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const result of sortedResults) {
|
for (const result of sortedResults) {
|
||||||
const typedResult = result as {
|
const typedResult = result as {
|
||||||
text?: string;
|
text?: string;
|
||||||
childrensectionids?: string[];
|
childrensectionids?: string[];
|
||||||
sectiontitle?: string;
|
sectiontitle?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typedResult.text) {
|
if (typedResult.text) {
|
||||||
allDocs.push(typedResult.text);
|
allDocs.push(typedResult.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add children to queue for recursive fetching only if recursive is true
|
// Add children to queue for recursive fetching only if recursive is true
|
||||||
if (
|
if (
|
||||||
recursive &&
|
recursive &&
|
||||||
typedResult.childrensectionids &&
|
typedResult.childrensectionids &&
|
||||||
Array.isArray(typedResult.childrensectionids)
|
Array.isArray(typedResult.childrensectionids)
|
||||||
) {
|
) {
|
||||||
queue.push(...typedResult.childrensectionids);
|
queue.push(...typedResult.childrensectionids);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return allDocs.join("\n\n---\n\n");
|
return {
|
||||||
},
|
content: allDocs.join("\n\n---\n\n"),
|
||||||
});
|
sectionCount: visited.size,
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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",
|
// Generate embedding for the query
|
||||||
description: toolMetadata.description,
|
const queryVector = await embeddings.embedQuery(query);
|
||||||
schema: searchSpecSchema,
|
|
||||||
func: async ({ query }) => {
|
|
||||||
// Generate embedding for the 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 };
|
||||||
},
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user