Repository navigation
feat(server): count which T3 MCP tools agents use #15913
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,13 +11,23 @@ import * as Schema from "effect/Schema"; | |
| import * as Sink from "effect/Sink"; | ||
| import * as Stream from "effect/Stream"; | ||
| import type * as Types from "effect/Types"; | ||
| import { McpProtocol, McpSchema, McpServer, Tool } from "effect/unstable/ai"; | ||
| import { McpProtocol, McpSchema, McpServer, Tool, type Toolkit } from "effect/unstable/ai"; | ||
| import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; | ||
| import { PreviewAutomationError } from "@t3tools/contracts"; | ||
| import { PreviewAutomationError, ThreadId } from "@t3tools/contracts"; | ||
|
|
||
| import packageJson from "../../package.json" with { type: "json" }; | ||
| import * as ServerConfig from "../config.ts"; | ||
| import * as DeviceService from "../device/DeviceService.ts"; | ||
| import * as ProviderRegistry from "../provider/Services/ProviderRegistry.ts"; | ||
| import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; | ||
| import { | ||
| type AgentThread, | ||
| agentToolProperties, | ||
| handoffSettings, | ||
| MAX_REPORTED_DEPTH, | ||
| toolOutcome, | ||
| } from "../telemetry/ProviderDimensions.ts"; | ||
| import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; | ||
| import * as McpInvocationContext from "./McpInvocationContext.ts"; | ||
| import * as OrchestratorMcpService from "./OrchestratorMcpService.ts"; | ||
| import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts"; | ||
|
|
@@ -647,61 +657,225 @@ const registerDeviceScreenshot = Effect.fn("McpHttpServer.registerDeviceScreensh | |
| ); | ||
| }); | ||
|
|
||
| const PreviewStandardToolkitRegistrationLive = McpServer.toolkit(PreviewStandardToolkit).pipe( | ||
| /** | ||
| * Tools that hand work to another thread's agent: they create it, message it, | ||
| * steer it, or move context into it. Their events name the receiving agent too. | ||
| */ | ||
| const HANDOFF_TOOLS: ReadonlySet<string> = new Set([ | ||
| "delegate_task", | ||
| "create_threads", | ||
| "t3_thread_launch", | ||
| "t3_thread_send", | ||
| "t3_thread_send_attachments", | ||
| "t3_thread_fork", | ||
| "t3_thread_merge_back", | ||
| "t3_queue_edit", | ||
| "t3_queue_promote_to_steer", | ||
| "t3_pending_request_respond", | ||
| "schedule_task", | ||
| "run_scheduled_task_now", | ||
| ]); | ||
|
|
||
| const resultThreadKeys = ["childThreadId", "targetThreadId", "boundThreadId", "threadId"] as const; | ||
|
|
||
| /** Thread ids a handoff tool result names: the thread that received the work. */ | ||
| const resultThreadIds = (content: unknown): ReadonlyArray<string> => { | ||
| if (typeof content !== "object" || content === null) return []; | ||
| const record = content as Readonly<Record<string, unknown>>; | ||
| if (Array.isArray(record.threads)) return record.threads.flatMap(resultThreadIds); | ||
| const key = resultThreadKeys.find((candidate) => typeof record[candidate] === "string"); | ||
| return key === undefined ? [] : [record[key] as string]; | ||
| }; | ||
|
|
||
| /** | ||
| * Runs a tool registration against a server that records one anonymous | ||
| * `mcp.tool.invoked` event per call: the tool, the calling agent's provider, | ||
| * the outcome with its failure code, and the duration. A handoff tool also | ||
| * reports the caller's model, origin, and delegation depth, the settings the | ||
| * agent chose, and the provider, model, and modes of each thread that received | ||
| * the work. Ids, prompts, and free text are never recorded. | ||
| */ | ||
| const withToolAnalytics = <E, R>(registration: Effect.Effect<void, E, R>) => | ||
| Effect.gen(function* () { | ||
| const server = yield* McpServer.McpServer; | ||
| const analytics = yield* Effect.serviceOption(AnalyticsService.AnalyticsService); | ||
| if (Option.isNone(analytics)) return yield* registration; | ||
| const registry = yield* Effect.serviceOption(ProviderRegistry.ProviderRegistry); | ||
| const threads = yield* Effect.serviceOption(ThreadManagement.ThreadManagementService); | ||
| const shellOf = (threadId: string) => | ||
| Option.isNone(threads) | ||
| ? Effect.succeed(null) | ||
| : threads.value | ||
| .getThreadShell(ThreadId.make(threadId)) | ||
| .pipe(Effect.orElseSucceed(() => null)); | ||
| // Delegation depth: parents walked from the caller, capped. | ||
| const depthOf = (thread: AgentThread) => | ||
| Effect.gen(function* () { | ||
| let depth = 0; | ||
| let parentId = | ||
| thread.lineage.relationshipToParent === "subagent" ? thread.lineage.parentThreadId : null; | ||
| while (parentId !== null && depth < MAX_REPORTED_DEPTH) { | ||
| depth += 1; | ||
| const parent = yield* shellOf(parentId); | ||
| parentId = | ||
| parent?.lineage.relationshipToParent === "subagent" | ||
| ? parent.lineage.parentThreadId | ||
| : null; | ||
| } | ||
| return depth; | ||
| }); | ||
| // Whether the caller's active run was started by a scheduled task. | ||
| const scheduledRunOf = (threadId: string, runId: string | null) => | ||
| Option.isNone(threads) || runId === null | ||
| ? Effect.succeed(false) | ||
| : threads.value | ||
| .getThreadRecords(ThreadId.make(threadId), ["runs", "messages"], { | ||
| runIds: [runId as never], | ||
| messageRoles: ["user"], | ||
| }) | ||
| .pipe( | ||
| Effect.map((records) => { | ||
| const run = records.runs.find((candidate) => candidate.id === runId); | ||
| return records.messages.some( | ||
| (message) => | ||
| message.id === run?.userMessageId && message.scheduledTaskId !== undefined, | ||
| ); | ||
| }), | ||
| Effect.orElseSucceed(() => false), | ||
| ); | ||
| const record = ( | ||
| tool: string, | ||
| args: unknown, | ||
| result: McpSchema.CallToolResult | undefined, | ||
| durationMs: number, | ||
| ) => | ||
| Effect.gen(function* () { | ||
| const invocation = Option.getOrUndefined( | ||
| yield* Effect.serviceOption(McpInvocationContext.McpInvocationContext), | ||
| ); | ||
| const providers = Option.isSome(registry) ? yield* registry.value.getProviders : []; | ||
| // Only handoff tools pay for thread lookups; the rest report the | ||
| // caller's provider from the credential. | ||
| const handoff = HANDOFF_TOOLS.has(tool); | ||
| const caller = | ||
| handoff && invocation !== undefined | ||
| ? ((yield* shellOf(invocation.threadId)) ?? undefined) | ||
| : undefined; | ||
| const targetIds = handoff | ||
| ? [...new Set(resultThreadIds(result?.structuredContent))].filter( | ||
| (threadId) => threadId !== invocation?.threadId, | ||
| ) | ||
| : []; | ||
|
Comment on lines
+764
to
+768
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- analytics wrapper ---'
nl -ba apps/server/src/mcp/McpHttpServer.ts | sed -n '640,810p'
printf '%s\n' '--- registrations / target-tool handlers ---'
nl -ba apps/server/src/mcp/toolkits/thread/handlers.ts | sed -n '1,135p'
printf '%s\n' '--- queue handlers ---'
rg -n -F -- 't3_queue_edit' apps/server/src/mcp
rg -n -F -- 't3_queue_promote_to_steer' apps/server/src/mcp
rg -n -F -- 't3_pending_request_respond' apps/server/src/mcp
printf '%s\n' '--- relevant contracts ---'
nl -ba packages/contracts/src/orchestratorMcp.ts | sed -n '490,535p'
nl -ba packages/contracts/src/orchestratorMcp.ts | sed -n '1,20p'
printf '%s\n' '--- provider/model telemetry consumers ---'
rg -n -F -- 'mcp.tool.invoked' apps packages || test "$?" -eq 1
rg -n -F -- 'providerId' apps/server/src/mcp/McpHttpServer.tsRepository: pingdotgg/t3code Length of output: 18635 🏁 Script executed: printf '%s\n' '--- handlers ---'
nl -ba apps/server/src/mcp/toolkits/thread/handlers.ts | sed -n '195,290p'
printf '%s\n' '--- tool schemas ---'
nl -ba apps/server/src/mcp/toolkits/thread/tools.ts | sed -n '90,180p'
printf '%s\n' '--- thread access ---'
nl -ba apps/server/src/mcp/threadAccess.ts | sed -n '1,115p'
printf '%s\n' '--- analytics property builder ---'
rg -n -F -- 'agentToolProperties' apps/server/src/mcp/McpHttpServer.ts
nl -ba apps/server/src/mcp/McpHttpServer.ts | sed -n '500,640p'
printf '%s\n' '--- invocation schema dispatch context ---'
rg -n -F -- 'McpInvocationContext' apps/server/src/mcpRepository: pingdotgg/t3code Length of output: 37244 🏁 Script executed: sed -n '1,45p' apps/server/src/mcp/McpHttpServer.ts
rg -n -F -- 'agentToolProperties' apps/server/src
rg -n -F -- 'function agentToolProperties' apps packages || test "$?" -eq 1
rg -n -F -- 'callerProviderInstanceId' apps/server/src packages || test "$?" -eq 1Repository: pingdotgg/t3code Length of output: 3833 🏁 Script executed: printf '%s\n' '--- analytics dimensions ---'
nl -ba apps/server/src/telemetry/ProviderDimensions.ts | sed -n '1,215p'
printf '%s\n' '--- relevant dimension tests ---'
nl -ba apps/server/src/telemetry/ProviderDimensions.test.ts | sed -n '35,145p'
printf '%s\n' '--- thread tool target schema ---'
nl -ba apps/server/src/mcp/toolkits/thread/tools.ts | sed -n '1,90p'Repository: pingdotgg/t3code Length of output: 16295 Use the explicit thread target for sequence-only handoffs. When Suggested fix const HANDOFF_TOOLS: ReadonlySet<string> = new Set([
"delegate_task",
"create_threads",
"t3_thread_launch",
"t3_thread_send",
"t3_thread_send_attachments",
"t3_thread_fork",
"t3_thread_merge_back",
"t3_queue_edit",
"t3_queue_promote_to_steer",
"t3_pending_request_respond",
"schedule_task",
"run_scheduled_task_now",
]);
+const SEQUENCE_ONLY_TARGET_TOOLS: ReadonlySet<string> = new Set([
+ "t3_queue_edit",
+ "t3_queue_promote_to_steer",
+ "t3_pending_request_respond",
+]);
+
const resultThreadKeys = ["childThreadId", "targetThreadId", "boundThreadId", "threadId"] as const;
@@
- const targetIds = handoff
- ? [...new Set(resultThreadIds(result?.structuredContent))].filter(
+ const resultIds = resultThreadIds(result?.structuredContent);
+ const argsRecord =
+ typeof args === "object" && args !== null
+ ? (args as Readonly<Record<string, unknown>>)
+ : {};
+ const explicitTargetIds =
+ resultIds.length === 0 &&
+ SEQUENCE_ONLY_TARGET_TOOLS.has(tool) &&
+ result !== undefined &&
+ toolOutcome(result).outcome === "ok" &&
+ typeof argsRecord.threadId === "string"
+ ? [argsRecord.threadId]
+ : [];
+ const targetIds = handoff
+ ? [...new Set([...resultIds, ...explicitTargetIds])].filter(
(threadId) => threadId !== invocation?.threadId,
)
: [];🤖 Prompt for AI Agents |
||
| const targets = yield* Effect.forEach(targetIds, shellOf); | ||
| for (const properties of agentToolProperties({ | ||
| tool, | ||
| providers, | ||
| caller, | ||
| ...(invocation === undefined | ||
| ? {} | ||
| : { callerProviderInstanceId: invocation.providerInstanceId }), | ||
| ...(caller === undefined || invocation === undefined | ||
| ? {} | ||
| : { | ||
| callerDepth: yield* depthOf(caller), | ||
| callerScheduledRun: yield* scheduledRunOf(invocation.threadId, caller.activeRunId), | ||
| }), | ||
| targets: targets.filter((thread) => thread !== null), | ||
| outcome: toolOutcome(result), | ||
| ...(handoff ? { settings: handoffSettings(tool, args, result?.structuredContent) } : {}), | ||
| durationMs, | ||
| })) { | ||
| yield* analytics.value.record("mcp.tool.invoked", properties); | ||
| } | ||
| }).pipe(Effect.ignoreCause); | ||
| const recordingServer = McpServer.McpServer.of({ | ||
| ...server, | ||
| addTool: (options) => | ||
| server.addTool({ | ||
| ...options, | ||
| handle: (payload) => | ||
| Effect.gen(function* () { | ||
| const startedAt = yield* Clock.currentTimeMillis; | ||
| return yield* options | ||
| .handle(payload) | ||
| .pipe( | ||
| Effect.onExit((exit) => | ||
| Clock.currentTimeMillis.pipe( | ||
| Effect.flatMap((endedAt) => | ||
| record( | ||
| options.tool.name, | ||
| payload, | ||
| exit._tag === "Success" ? exit.value : undefined, | ||
| endedAt - startedAt, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }), | ||
| }), | ||
| }); | ||
| return yield* registration.pipe(Effect.provideService(McpServer.McpServer, recordingServer)); | ||
| }); | ||
|
|
||
| const toolkit = <Tools extends Record<string, Tool.Any>>(tools: Toolkit.Toolkit<Tools>) => | ||
| Layer.effectDiscard(withToolAnalytics(McpServer.registerToolkit(tools))).pipe( | ||
| Layer.provide(McpServer.McpServer.layer), | ||
| ); | ||
|
|
||
| const PreviewStandardToolkitRegistrationLive = toolkit(PreviewStandardToolkit).pipe( | ||
| Layer.provide(PreviewStandardToolkitHandlersLive), | ||
| ); | ||
|
|
||
| const PreviewSnapshotRegistrationLive = Layer.effectDiscard(registerPreviewSnapshot()).pipe( | ||
| Layer.provide(PreviewSnapshotToolkitHandlersLive), | ||
| ); | ||
| const PreviewSnapshotRegistrationLive = Layer.effectDiscard( | ||
| withToolAnalytics(registerPreviewSnapshot()), | ||
| ).pipe(Layer.provide(PreviewSnapshotToolkitHandlersLive)); | ||
|
|
||
| export const PreviewToolkitRegistrationLive = Layer.mergeAll( | ||
| PreviewStandardToolkitRegistrationLive, | ||
| PreviewSnapshotRegistrationLive, | ||
| ); | ||
|
|
||
| export const OrchestratorToolkitRegistrationLive = McpServer.toolkit(OrchestratorToolkit).pipe( | ||
| export const OrchestratorToolkitRegistrationLive = toolkit(OrchestratorToolkit).pipe( | ||
| Layer.provide(OrchestratorToolkitHandlersLive), | ||
| Layer.provide(OrchestratorMcpService.layer), | ||
| Layer.provide(ThreadMetadataMcpService.layer), | ||
| ); | ||
|
|
||
| export const ThreadToolkitRegistrationLive = McpServer.toolkit(ThreadToolkit).pipe( | ||
| export const ThreadToolkitRegistrationLive = toolkit(ThreadToolkit).pipe( | ||
| Layer.provide(ThreadToolkitHandlersLive), | ||
| ); | ||
|
|
||
| const WorktreeToolkitRegistrationLive = McpServer.toolkit(WorktreeToolkit).pipe( | ||
| const WorktreeToolkitRegistrationLive = toolkit(WorktreeToolkit).pipe( | ||
| Layer.provide(WorktreeToolkitHandlersLive), | ||
| Layer.provide(WorktreeMcpService.layer), | ||
| ); | ||
|
|
||
| const PreviewControlsRegistrationLive = McpServer.toolkit(PreviewControlsToolkit).pipe( | ||
| const PreviewControlsRegistrationLive = toolkit(PreviewControlsToolkit).pipe( | ||
| Layer.provide(PreviewControlsHandlersLive), | ||
| ); | ||
|
|
||
| const EnvironmentRegistrationLive = McpServer.toolkit(EnvironmentToolkit).pipe( | ||
| const EnvironmentRegistrationLive = toolkit(EnvironmentToolkit).pipe( | ||
| Layer.provide(EnvironmentHandlersLive), | ||
| ); | ||
|
|
||
| const ProjectRegistrationLive = McpServer.toolkit(ProjectToolkit).pipe( | ||
| Layer.provide(ProjectHandlersLive), | ||
| ); | ||
| const ProjectRegistrationLive = toolkit(ProjectToolkit).pipe(Layer.provide(ProjectHandlersLive)); | ||
|
|
||
| const AttachmentRegistrationLive = McpServer.toolkit(AttachmentToolkit).pipe( | ||
| const AttachmentRegistrationLive = toolkit(AttachmentToolkit).pipe( | ||
| Layer.provide(AttachmentHandlersLive), | ||
| ); | ||
|
|
||
| export const PullRequestsToolkitRegistrationLive = McpServer.toolkit(PullRequestsToolkit).pipe( | ||
| export const PullRequestsToolkitRegistrationLive = toolkit(PullRequestsToolkit).pipe( | ||
| Layer.provide(PullRequestsToolkitHandlersLive), | ||
| ); | ||
|
|
||
| const DeviceStandardToolkitRegistrationLive = McpServer.toolkit(DeviceStandardToolkit).pipe( | ||
| const DeviceStandardToolkitRegistrationLive = toolkit(DeviceStandardToolkit).pipe( | ||
| Layer.provide(DeviceStandardToolkitHandlersLive), | ||
| ); | ||
|
|
||
| const DeviceScreenshotRegistrationLive = Layer.effectDiscard(registerDeviceScreenshot()).pipe( | ||
| Layer.provide(DeviceScreenshotToolkitHandlersLive), | ||
| ); | ||
| const DeviceScreenshotRegistrationLive = Layer.effectDiscard( | ||
| withToolAnalytics(registerDeviceScreenshot()), | ||
| ).pipe(Layer.provide(DeviceScreenshotToolkitHandlersLive)); | ||
|
|
||
| export const DeviceToolkitRegistrationLive = Layer.mergeAll( | ||
| DeviceStandardToolkitRegistrationLive, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
mcp/McpHttpServer.ts:764Calls to
t3_queue_edit,t3_queue_promote_to_steer, andt3_pending_request_respondtargeting another thread emit only the caller/base analytics event, without the target provider, model, orcrossProviderdimensions. Their handlers return only{ sequence }, soresultThreadIds(result?.structuredContent)produces no target IDs here; include the resolved target ID in the result or derive it from the validated invocation before performing this lookup.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: