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:
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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