Skip to content

Commit eeb3a3f

Browse files
Shaik-SirajuddinDanielSLewepinzur
authored
feat: enable tracing for tool executions through mcp server (#12804)
## Description <!-- Provide a brief description of the changes in this PR --> The Pr udpates core tool to create a default span if not passed , which enables tracing for all core tool executions one of output is Observability section in UI , traces are now logged during execution of mcp server tools ## Related Issue(s) <!-- Link to the issue(s) this PR addresses, using hashtag notation: Fixes #123 --> ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test update ## Checklist - [ ] I have made corresponding changes to the documentation (if applicable) - [ ] I have added tests that prove my fix is effective or that my feature works <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * MCP server tool executions now generate observable traces that appear in the Observability UI, providing enhanced visibility into tool performance and execution flow. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Daniel Lew <danielshlomolew@gmail.com> Co-authored-by: Daniel Lew <51924260+DanielSLew@users.noreply.github.com> Co-authored-by: Eric Pinzur <2641606+epinzur@users.noreply.github.com>
1 parent c8f4e36 commit eeb3a3f

6 files changed

Lines changed: 119 additions & 28 deletions

File tree

.changeset/floppy-paths-do.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@mastra/core': minor
3+
---
4+
Enabled tracing for tool executions through mcp server
5+
6+
Traces now appear in the Observability UI for MCP server tool calls

observability/mastra/src/integration-tests.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { StructuredOutputOptions } from '@mastra/core/agent';
44
import type { MastraDBMessage } from '@mastra/core/agent/message-list';
55
import { RequestContext } from '@mastra/core/di';
66
import { Mastra } from '@mastra/core/mastra';
7-
import { SpanType } from '@mastra/core/observability';
7+
import { SpanType, EntityType, getOrCreateSpan, executeWithContext } from '@mastra/core/observability';
88
import type { TracingContext } from '@mastra/core/observability';
99

1010
// Core Mastra imports
@@ -1838,4 +1838,73 @@ describe('Tracing Integration Tests', () => {
18381838
finalExpectations(testExporter);
18391839
});
18401840
});
1841+
1842+
describe('Standalone tool execution tracing (MCP-style)', () => {
1843+
it('should create a root span when tool is executed without a parent span context', async () => {
1844+
const testExporter = new TestExporter();
1845+
1846+
const simpleTool = createTool({
1847+
id: 'standalone-tool',
1848+
description: 'A tool executed without an agent',
1849+
inputSchema: z.object({ value: z.number() }),
1850+
outputSchema: z.object({ doubled: z.number() }),
1851+
execute: async inputData => {
1852+
return { doubled: inputData.value * 2 };
1853+
},
1854+
});
1855+
1856+
const mastra = new Mastra({
1857+
...getBaseMastraConfig(testExporter),
1858+
tools: { 'standalone-tool': simpleTool },
1859+
});
1860+
1861+
// Simulate standalone tool execution (e.g. MCP server calling a tool directly)
1862+
// by using getOrCreateSpan with no tracingContext.currentSpan
1863+
const selectedInstance = mastra.observability.getSelectedInstance({});
1864+
expect(selectedInstance).toBeDefined();
1865+
expect(typeof selectedInstance!.startSpan).toBe('function');
1866+
1867+
const toolSpan = getOrCreateSpan({
1868+
type: SpanType.TOOL_CALL,
1869+
name: "tool: 'standalone-tool'",
1870+
input: { value: 5 },
1871+
entityType: EntityType.TOOL,
1872+
entityId: 'standalone-tool',
1873+
entityName: 'standalone-tool',
1874+
attributes: {
1875+
toolDescription: 'A tool executed without an agent',
1876+
toolType: 'tool',
1877+
},
1878+
tracingContext: { currentSpan: undefined },
1879+
mastra,
1880+
});
1881+
1882+
expect(toolSpan).toBeDefined();
1883+
1884+
// Execute within the span context and complete it
1885+
const result = await executeWithContext({
1886+
span: toolSpan!,
1887+
fn: async () => {
1888+
return { doubled: 10 };
1889+
},
1890+
});
1891+
1892+
toolSpan!.end({ output: result });
1893+
1894+
// Flush the observability bus to ensure async export handlers complete
1895+
await selectedInstance!.getObservabilityBus().flush();
1896+
1897+
expect(result).toEqual({ doubled: 10 });
1898+
1899+
// Verify a TOOL_CALL span was created as a root span
1900+
const toolSpans = testExporter.getSpansByType(SpanType.TOOL_CALL);
1901+
expect(toolSpans).toHaveLength(1);
1902+
expect(toolSpans[0]?.name).toBe("tool: 'standalone-tool'");
1903+
expect(toolSpans[0]?.traceId).toBeDefined();
1904+
1905+
// Verify no incomplete spans
1906+
const incompleteSpans = testExporter.getIncompleteSpans();
1907+
expect(incompleteSpans).toHaveLength(0);
1908+
});
1909+
});
18411910
});

observability/mastra/src/spans/default.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -153,34 +153,34 @@ export class DefaultSpan<TType extends SpanType> extends BaseSpan<TType> {
153153
/**
154154
* Generate OpenTelemetry-compatible span ID (64-bit, 16 hex chars)
155155
*/
156-
function generateSpanId(): string {
157-
// Generate 8 random bytes (64 bits) in hex format
158-
const bytes = new Uint8Array(8);
159-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
160-
crypto.getRandomValues(bytes);
161-
} else {
162-
// Fallback for environments without crypto.getRandomValues
163-
for (let i = 0; i < 8; i++) {
164-
bytes[i] = Math.floor(Math.random() * 256);
156+
function fillRandomBytes(bytes: Uint8Array): void {
157+
try {
158+
// Use Web Crypto API with proper this binding
159+
const webCrypto = globalThis.crypto;
160+
if (webCrypto?.getRandomValues) {
161+
webCrypto.getRandomValues.call(webCrypto, bytes);
162+
return;
165163
}
164+
} catch {
165+
// Fall through to fallback
166166
}
167+
for (let i = 0; i < bytes.length; i++) {
168+
bytes[i] = Math.floor(Math.random() * 256);
169+
}
170+
}
171+
172+
function generateSpanId(): string {
173+
const bytes = new Uint8Array(8);
174+
fillRandomBytes(bytes);
167175
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
168176
}
169177

170178
/**
171179
* Generate OpenTelemetry-compatible trace ID (128-bit, 32 hex chars)
172180
*/
173181
function generateTraceId(): string {
174-
// Generate 16 random bytes (128 bits) in hex format
175182
const bytes = new Uint8Array(16);
176-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
177-
crypto.getRandomValues(bytes);
178-
} else {
179-
// Fallback for environments without crypto.getRandomValues
180-
for (let i = 0; i < 16; i++) {
181-
bytes[i] = Math.floor(Math.random() * 256);
182-
}
183-
}
183+
fillRandomBytes(bytes);
184184
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
185185
}
186186

packages/core/src/tools/tool-builder/builder.e2e.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,8 @@ describe('Tool Tracing Context Injection', () => {
609609
entityType: 'tool',
610610
requestContext: new RequestContext(),
611611
tracingPolicy: undefined,
612+
mastra: undefined,
613+
metadata: {},
612614
});
613615

614616
// Verify tracingContext was injected with the tool span
@@ -623,7 +625,7 @@ describe('Tool Tracing Context Injection', () => {
623625
expect(result).toEqual({ result: 'processed: test' });
624626
});
625627

626-
it('should not inject tracingContext when agentSpan is not available', async () => {
628+
it('should not inject tracingContext when agentSpan is not available and no observability configured', async () => {
627629
let receivedTracingContext: any = undefined;
628630

629631
const testTool = createTool({
@@ -655,7 +657,7 @@ describe('Tool Tracing Context Injection', () => {
655657
const builtTool = builder.build();
656658
const result = await builtTool.execute!({ message: 'test' }, { toolCallId: 'test-call-id', messages: [] });
657659

658-
// Verify tracingContext was injected but currentSpan is undefined
660+
// Verify tracingContext was injected but currentSpan is undefined (no observability configured)
659661
expect(receivedTracingContext).toEqual({ currentSpan: undefined });
660662
expect(result).toEqual({ result: 'processed: test' });
661663
});
@@ -716,6 +718,8 @@ describe('Tool Tracing Context Injection', () => {
716718
entityType: 'tool',
717719
requestContext: new RequestContext(),
718720
tracingPolicy: undefined,
721+
mastra: undefined,
722+
metadata: {},
719723
});
720724

721725
// Verify Vercel tool execute was called (without tracingContext)
@@ -837,6 +841,8 @@ describe('Tool Tracing Context Injection', () => {
837841
entityType: 'tool',
838842
requestContext: new RequestContext(),
839843
tracingPolicy: undefined,
844+
mastra: undefined,
845+
metadata: {},
840846
});
841847
});
842848
});

packages/core/src/tools/tool-builder/builder.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ import {
1111
jsonSchema,
1212
} from '@mastra/schema-compat';
1313
import { z } from 'zod/v4';
14+
import { Mastra } from '../..';
1415
import { MastraBase } from '../../base';
1516
import { ErrorCategory, MastraError, ErrorDomain } from '../../error';
16-
import { SpanType, wrapMastra, executeWithContext, EntityType, createObservabilityContext } from '../../observability';
17+
import {
18+
SpanType,
19+
wrapMastra,
20+
executeWithContext,
21+
EntityType,
22+
getOrCreateSpan,
23+
createObservabilityContext,
24+
} from '../../observability';
1725
import { RequestContext } from '../../request-context';
1826
import { isStandardSchemaWithJSON, toStandardSchema, standardSchemaToJSONSchema } from '../../schema';
1927
import { isVercelTool } from '../../tools/toolchecks';
@@ -321,9 +329,9 @@ export class CoreToolBuilder extends MastraBase {
321329
// Fall back to build-time context for Legacy methods (AI SDK v4 doesn't support passing custom options)
322330
const tracingContext = execOptions.tracingContext || options.tracingContext;
323331

324-
// Create tool span if we have a current span available
332+
// Create tool span - either as child of existing span or as new root span (e.g. MCP tools)
325333
const toolRequestContext = execOptions.requestContext ?? options.requestContext;
326-
const toolSpan = tracingContext?.currentSpan?.createChildSpan({
334+
const toolSpan = getOrCreateSpan({
327335
type: SpanType.TOOL_CALL,
328336
name: `tool: '${options.name}'`,
329337
input: args,
@@ -335,7 +343,9 @@ export class CoreToolBuilder extends MastraBase {
335343
toolType: logType || 'tool',
336344
},
337345
tracingPolicy: options.tracingPolicy,
346+
tracingContext: tracingContext,
338347
requestContext: toolRequestContext,
348+
mastra: options.mastra instanceof Mastra ? options.mastra : undefined,
339349
});
340350

341351
try {

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)