mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 21:31:46 +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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user