Files
ask262/src/mcp-server-stdio.ts
T
bendtherules d71726205d feat(langfuse): add tool-level spans with input/output and trace-level I/O
Each MCP tool handler now sets:
- LANGFUSE_TRACE_OUTPUT_ATTR on tool spans (maps to top-level trace output)
- LANGFUSE_OBSERVATION_INPUT_ATTR and LANGFUSE_OBSERVATION_OUTPUT_ATTR
  for detailed child observation views

HTTP root span renamed to 'mcp_http_request' for consistency with stdio.
All attribute keys use constants from langfuse-transport.ts.
2026-04-27 18:56:50 +05:30

335 lines
9.3 KiB
TypeScript

#!/usr/bin/env bun
/**
* Ask262 MCP Server
* Provides MCP-compatible tools for exploring the ECMAScript specification.
*/
import path from "node:path";
import { fileURLToPath } from "node:url";
import * as lancedbSdk from "@lancedb/lancedb";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { askPromptMetadata, createAskPrompt } from "./agent-prompts/index.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 as STORAGE_DIR_REL } from "./constants.js";
import { createEmbeddings } from "./lib/embeddings-factory.js";
import { LogOperation, logger } from "./lib/logger.js";
import { trace } from "@opentelemetry/api";
import {
LANGFUSE_OBSERVATION_INPUT_ATTR,
LANGFUSE_OBSERVATION_OUTPUT_ATTR,
LANGFUSE_TRACE_INPUT_ATTR,
LANGFUSE_TRACE_NAME_ATTR,
LANGFUSE_TRACE_OUTPUT_ATTR,
TRACE_NAME_STDIO,
} from "./lib/langfuse-transport.js";
import {
createProcessScopedTrace,
getSessionMetadata,
setupTracing,
withSpanContext,
} from "./lib/tracing.js";
// Resolve storage path relative to this script's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const STORAGE_DIR = path.resolve(__dirname, "..", STORAGE_DIR_REL);
// #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 based on ASK262_EMBEDDING_PROVIDER env var
const embeddings = createEmbeddings();
export async function main() {
// Initialize tracing (registers Langfuse processor when enabled)
setupTracing();
// Initialize stdio server logger
const log = await logger.forComponent("stdio-server");
// Create process-scoped trace ID for this session
const sessionTraceId = createProcessScopedTrace();
log.info(LogOperation.SERVER_STARTED, {
transport: "stdio",
trace_id: sessionTraceId,
});
// 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",
},
{
capabilities: {
prompts: {},
},
},
);
// 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> => {
return await withSpanContext(
sessionTraceId,
"ask262_search_spec_sections",
{
LANGFUSE_TRACE_NAME_ATTR: TRACE_NAME_STDIO,
LANGFUSE_TRACE_INPUT_ATTR: JSON.stringify({
tool: searchSpecToolName,
input: { query },
}),
LANGFUSE_OBSERVATION_INPUT_ATTR: JSON.stringify({ query }),
tool: searchSpecToolName,
query,
},
async () => {
const result = await searchSpecTool({ query });
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_OBSERVATION_OUTPUT_ATTR,
JSON.stringify(result),
);
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_TRACE_OUTPUT_ATTR,
JSON.stringify(result),
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
getSessionMetadata("stdio"),
);
},
);
// Register get section content tool
server.registerTool(
sectionContentToolName,
{
description: sectionContentToolMetadata.description,
inputSchema: getSectionInputSchema,
outputSchema: getSectionOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
async ({
sectionIds,
recursive,
}: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => {
return await withSpanContext(
sessionTraceId,
"ask262_get_section_content",
{
LANGFUSE_TRACE_NAME_ATTR: TRACE_NAME_STDIO,
LANGFUSE_TRACE_INPUT_ATTR: JSON.stringify({
tool: sectionContentToolName,
input: { sectionIds, recursive },
}),
LANGFUSE_OBSERVATION_INPUT_ATTR: JSON.stringify({
sectionIds,
recursive,
}),
tool: sectionContentToolName,
section_count: sectionIds.length,
},
async () => {
const result = await getSectionContentTool({ sectionIds, recursive });
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_OBSERVATION_OUTPUT_ATTR,
JSON.stringify(result),
);
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_TRACE_OUTPUT_ATTR,
JSON.stringify(result),
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
getSessionMetadata("stdio"),
);
},
);
// Register evaluate in engine262 tool
server.registerTool(
evaluateToolName,
{
description: evaluateToolMetadata.description,
inputSchema: evaluateInputSchema,
outputSchema: evaluateOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
async ({ code }: EvaluateToolMCPInput): Promise<EvaluateToolMCPOutput> => {
return await withSpanContext(
sessionTraceId,
"ask262_evaluate_in_engine262",
{
LANGFUSE_TRACE_NAME_ATTR: TRACE_NAME_STDIO,
LANGFUSE_TRACE_INPUT_ATTR: JSON.stringify({
tool: evaluateToolName,
input: { code },
}),
LANGFUSE_OBSERVATION_INPUT_ATTR: JSON.stringify({ code }),
tool: evaluateToolName,
code_length: code.length,
},
async () => {
const result = await evaluateTool({ code });
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_OBSERVATION_OUTPUT_ATTR,
JSON.stringify(result),
);
trace
.getActiveSpan()
?.setAttribute(
LANGFUSE_TRACE_OUTPUT_ATTR,
JSON.stringify(result),
);
const isError = result.error !== undefined;
const text = isError ? result.error : JSON.stringify(result, null, 2);
return {
content: [{ type: "text", text }],
structuredContent: result,
isError,
};
},
getSessionMetadata("stdio"),
);
},
);
// Register prompt for tool orchestration guidance
server.registerPrompt("ask", askPromptMetadata, async ({ question }) =>
createAskPrompt(question),
);
// Use stdio transport for communication
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Ask262 MCP Server running on stdio");
// Handle graceful shutdown
const shutdown = (signal: string) => {
log.info(LogOperation.SERVER_STOPPED, {
signal,
trace_id: sessionTraceId,
});
process.exit(0);
};
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});