feat(langfuse): add tool-level spans with input/output to traces

Wrap each MCP tool handler in withSpan()/withSpanContext() to create
individual named spans per tool call:
- ask262_search_spec_sections
- ask262_get_section_content
- ask262_evaluate_in_engine262

Each span includes:
- langfuse.observation.input: JSON of tool arguments (query, sectionIds, code)
- tool name and relevant metadata as span attributes

This makes the Langfuse trace hierarchy show which MCP tool was called
and what input it received, nested under the root HTTP/stdio request span.
This commit is contained in:
2026-04-27 18:31:25 +05:30
parent 627b80008e
commit 54bc20cb7f
2 changed files with 98 additions and 28 deletions
+63 -21
View File
@@ -36,7 +36,7 @@ import {
import { DEFAULT_PORT, STORAGE_DIR as STORAGE_DIR_REL } from "./constants.js"; import { DEFAULT_PORT, STORAGE_DIR as STORAGE_DIR_REL } from "./constants.js";
import { createEmbeddings } from "./lib/embeddings-factory.js"; import { createEmbeddings } from "./lib/embeddings-factory.js";
import { LogOperation, logger } from "./lib/logger.js"; import { LogOperation, logger } from "./lib/logger.js";
import { withSpan } from "./lib/tracing.js"; import { getSessionMetadata, setupTracing, withSpan } from "./lib/tracing.js";
// Resolve storage path relative to this script's directory // Resolve storage path relative to this script's directory
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -90,12 +90,22 @@ async function createMcpServer() {
}, },
}, },
async ({ query }) => { async ({ query }) => {
const result = await searchSpecTool({ query }); return await withSpan(
return { "ask262_search_spec_sections",
content: [{ type: "text", text: JSON.stringify(result, null, 2) }], {
structuredContent: result, "langfuse.observation.input": JSON.stringify({ query }),
isError: false, tool: searchSpecToolName,
}; query,
},
async () => {
const result = await searchSpecTool({ query });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
);
}, },
); );
@@ -113,12 +123,25 @@ async function createMcpServer() {
}, },
}, },
async ({ sectionIds, recursive }) => { async ({ sectionIds, recursive }) => {
const result = await getSectionContentTool({ sectionIds, recursive }); return await withSpan(
return { "ask262_get_section_content",
content: [{ type: "text", text: JSON.stringify(result, null, 2) }], {
structuredContent: result, "langfuse.observation.input": JSON.stringify({
isError: false, sectionIds,
}; recursive,
}),
tool: sectionContentToolName,
section_count: sectionIds.length,
},
async () => {
const result = await getSectionContentTool({ sectionIds, recursive });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result,
isError: false,
};
},
);
}, },
); );
@@ -136,14 +159,28 @@ async function createMcpServer() {
}, },
}, },
async ({ code }) => { async ({ code }) => {
const result = await evaluateTool({ code }); return await withSpan(
const isError = result.error !== undefined; "ask262_evaluate_in_engine262",
const text = isError ? result.error : JSON.stringify(result, null, 2); {
return { "langfuse.observation.input": JSON.stringify({
content: [{ type: "text", text }], code: code.slice(0, 200),
structuredContent: result, }),
isError, tool: evaluateToolName,
}; code_length: code.length,
},
async () => {
const result = await evaluateTool({ code });
const isError = result.error !== undefined;
const text = isError
? result.error
: JSON.stringify(result, null, 2);
return {
content: [{ type: "text", text }],
structuredContent: result,
isError,
};
},
);
}, },
); );
@@ -156,6 +193,9 @@ async function createMcpServer() {
} }
export async function main() { export async function main() {
// Initialize tracing (registers Langfuse processor when enabled)
setupTracing();
// Initialize HTTP server logger // Initialize HTTP server logger
const log = await logger.forComponent("http-server"); const log = await logger.forComponent("http-server");
@@ -217,6 +257,7 @@ export async function main() {
} }
// Handle request within trace context (passing trace ID from header if available) // Handle request within trace context (passing trace ID from header if available)
const sessionMetadata = getSessionMetadata("http");
return await withSpan( return await withSpan(
LogOperation.HANDLING_MCP_HTTP_REQUEST, LogOperation.HANDLING_MCP_HTTP_REQUEST,
{ method: c.req.method, client_ip: clientIp }, { method: c.req.method, client_ip: clientIp },
@@ -257,6 +298,7 @@ export async function main() {
} }
}, },
traceId, traceId,
sessionMetadata,
); );
}); });
+35 -7
View File
@@ -37,7 +37,12 @@ import {
import { STORAGE_DIR as STORAGE_DIR_REL } from "./constants.js"; import { STORAGE_DIR as STORAGE_DIR_REL } from "./constants.js";
import { createEmbeddings } from "./lib/embeddings-factory.js"; import { createEmbeddings } from "./lib/embeddings-factory.js";
import { LogOperation, logger } from "./lib/logger.js"; import { LogOperation, logger } from "./lib/logger.js";
import { createProcessScopedTrace, withSpanContext } from "./lib/tracing.js"; import {
createProcessScopedTrace,
getSessionMetadata,
setupTracing,
withSpanContext,
} from "./lib/tracing.js";
// Resolve storage path relative to this script's directory // Resolve storage path relative to this script's directory
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -90,6 +95,9 @@ export interface SearchSpecMCPOutput extends McpToolOutputBase {
const embeddings = createEmbeddings(); const embeddings = createEmbeddings();
export async function main() { export async function main() {
// Initialize tracing (registers Langfuse processor when enabled)
setupTracing();
// Initialize stdio server logger // Initialize stdio server logger
const log = await logger.forComponent("stdio-server"); const log = await logger.forComponent("stdio-server");
@@ -139,8 +147,12 @@ export async function main() {
async ({ query }: SearchSpecMCPInput): Promise<SearchSpecMCPOutput> => { async ({ query }: SearchSpecMCPInput): Promise<SearchSpecMCPOutput> => {
return await withSpanContext( return await withSpanContext(
sessionTraceId, sessionTraceId,
"vector_search", "ask262_search_spec_sections",
{ tool: searchSpecToolName, query }, {
"langfuse.observation.input": JSON.stringify({ query }),
tool: searchSpecToolName,
query,
},
async () => { async () => {
const result = await searchSpecTool({ query }); const result = await searchSpecTool({ query });
return { return {
@@ -149,6 +161,7 @@ export async function main() {
isError: false, isError: false,
}; };
}, },
getSessionMetadata("stdio"),
); );
}, },
); );
@@ -172,8 +185,15 @@ export async function main() {
}: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => { }: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => {
return await withSpanContext( return await withSpanContext(
sessionTraceId, sessionTraceId,
"section_fetch", "ask262_get_section_content",
{ tool: sectionContentToolName, section_count: sectionIds.length }, {
"langfuse.observation.input": JSON.stringify({
sectionIds,
recursive,
}),
tool: sectionContentToolName,
section_count: sectionIds.length,
},
async () => { async () => {
const result = await getSectionContentTool({ sectionIds, recursive }); const result = await getSectionContentTool({ sectionIds, recursive });
return { return {
@@ -182,6 +202,7 @@ export async function main() {
isError: false, isError: false,
}; };
}, },
getSessionMetadata("stdio"),
); );
}, },
); );
@@ -202,8 +223,14 @@ export async function main() {
async ({ code }: EvaluateToolMCPInput): Promise<EvaluateToolMCPOutput> => { async ({ code }: EvaluateToolMCPInput): Promise<EvaluateToolMCPOutput> => {
return await withSpanContext( return await withSpanContext(
sessionTraceId, sessionTraceId,
"code_execution", "ask262_evaluate_in_engine262",
{ tool: evaluateToolName, code_length: code.length }, {
"langfuse.observation.input": JSON.stringify({
code: code.slice(0, 200),
}),
tool: evaluateToolName,
code_length: code.length,
},
async () => { async () => {
const result = await evaluateTool({ code }); const result = await evaluateTool({ code });
const isError = result.error !== undefined; const isError = result.error !== undefined;
@@ -214,6 +241,7 @@ export async function main() {
isError, isError,
}; };
}, },
getSessionMetadata("stdio"),
); );
}, },
); );