| import { readJsonFromUpstash } from '../_upstash-json.js'; |
| |
| import { captureSilentError } from '../_sentry-edge.js'; |
| import { secondsUntilUtcMidnight } from '../../server/_shared/pro-mcp-token'; |
| import { getMcpBillingVerificationDenial } from './auth'; |
| import { BillingDenialError } from './billing-denial'; |
| import { |
| createMcpToolExecutionContext, |
| downstreamErrorTags, |
| } from './downstream'; |
| import { mcpErrorFingerprint } from './error-fingerprint'; |
| import { argBool, summarizeData } from './filters'; |
| import { evaluateFreshness } from './freshness'; |
| import { applyJmespath } from './jmespath'; |
| import { reserveQuota } from './quota'; |
| import { TOOL_REGISTRY } from './registry/index'; |
| import { rpcError, rpcOk, withMcpNoStore } from './rpc'; |
| import { McpSourceUnavailableError } from './source-unavailable'; |
| import { |
| emitTelemetry, |
| principalIdForLog, |
| telemetryEnabled, |
| } from './telemetry'; |
| import type { |
| CacheToolDef, |
| McpAuthContext, |
| McpHandlerDeps, |
| McpToolExecutionContext, |
| } from './types'; |
| import { utf8ByteLength } from './utils'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function executeTool( |
| tool: CacheToolDef, |
| params: Record<string, unknown> = {}, |
| ): Promise<{ cached_at: string | null; stale: boolean; data: Record<string, unknown> }> { |
| const reads = tool._cacheKeys.map(k => readJsonFromUpstash(k)); |
| const freshnessChecks = tool._freshnessChecks?.length |
| ? tool._freshnessChecks |
| : [{ key: tool._seedMetaKey, maxStaleMin: tool._maxStaleMin }]; |
| const metaReads = freshnessChecks.map((check) => readJsonFromUpstash(check.key)); |
| const [results, metas] = await Promise.all([Promise.all(reads), Promise.all(metaReads)]); |
| const { cached_at, stale } = evaluateFreshness(freshnessChecks, metas); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| tool._cacheKeys.length > 0 && |
| results.every((v: unknown) => v === null || v === undefined) |
| ) { |
| throw new Error('cache_all_null'); |
| } |
|
|
| const data: Record<string, unknown> = {}; |
| |
| |
| const NON_LABEL = /^(v\d+|\d+|stale|sebuf)$/; |
| tool._cacheKeys.forEach((key, i) => { |
| const parts = key.split(':'); |
| let label = ''; |
| for (let idx = parts.length - 1; idx >= 0; idx--) { |
| const seg = parts[idx] ?? ''; |
| if (!NON_LABEL.test(seg)) { label = seg; break; } |
| } |
| data[tool._cacheLabels?.[key] || label || (parts[0] ?? key)] = results[i]; |
| }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let result: Record<string, unknown> = data; |
| if (tool._postFilter) { |
| try { |
| result = tool._postFilter(structuredClone(data), params); |
| } catch (err) { |
| |
| |
| |
| captureSilentError(err, { |
| tags: { route: 'api/mcp', step: 'post-filter', tool: tool.name }, |
| fingerprint: mcpErrorFingerprint('post-filter', tool.name, err), |
| }); |
| result = data; |
| } |
| } |
|
|
| |
| |
| |
| |
| if (argBool(params.summary)) result = tool._summarize ? tool._summarize(result) : summarizeData(result); |
|
|
| return { cached_at, stale, data: result }; |
| } |
|
|
| export async function dispatchToolsCall( |
| req: Request, |
| context: McpAuthContext, |
| deps: McpHandlerDeps, |
| body: { id?: unknown; params?: unknown }, |
| corsHeaders: Record<string, string>, |
| ctx?: { waitUntil: (p: Promise<unknown>) => void }, |
| |
| |
| |
| |
| mcpDailyLimit?: number | null, |
| ): Promise<Response> { |
| const id = body.id ?? null; |
| const p = body.params as { name?: string; arguments?: Record<string, unknown> } | null; |
| if (!p || typeof p.name !== 'string') { |
| return rpcError(id, -32602, 'Invalid params: missing tool name', corsHeaders); |
| } |
| const tool = TOOL_REGISTRY.find((t) => t.name === p.name); |
| if (!tool) { |
| return rpcError(id, -32602, `Unknown tool: ${p.name}`, corsHeaders); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const isMetadataTool = p.name === 'describe_tool'; |
| |
| |
| |
| |
| |
| |
| if ((context.kind === 'pro' || context.kind === 'user_key') && !isMetadataTool) { |
| const reservation = await reserveQuota(context.userId, deps.redisPipeline, mcpDailyLimit); |
| if (!reservation.ok) { |
| if (reservation.reason === 'cap-exceeded') { |
| |
| |
| return new Response( |
| JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32029, message: `Daily MCP quota exceeded (${reservation.floor}/day). Resets at next UTC midnight.` } }), |
| { status: 429, headers: withMcpNoStore({ 'Content-Type': 'application/json', 'Retry-After': String(secondsUntilUtcMidnight()), ...corsHeaders }) }, |
| ); |
| } |
| |
| return new Response( |
| JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Service temporarily unavailable, retry in a moment.' } }), |
| { status: 503, headers: withMcpNoStore({ 'Content-Type': 'application/json', 'Retry-After': '5', ...corsHeaders }) }, |
| ); |
| } |
| |
| |
| |
| } |
|
|
| const jmespathArg = p.arguments?.jmespath; |
| const jmespathUsed = typeof jmespathArg === 'string' && jmespathArg.length > 0; |
| |
| |
| |
| |
| |
| const tStart = Date.now(); |
| let execution: McpToolExecutionContext | undefined; |
| try { |
| let result: unknown; |
| if (tool._execute) { |
| execution = createMcpToolExecutionContext(req.url); |
| result = await tool._execute( |
| p.arguments ?? {}, |
| execution.downstreamOrigin, |
| context, |
| execution, |
| ); |
| } else { |
| result = await executeTool(tool, p.arguments ?? {}); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const { text, failed } = applyJmespath(result, jmespathArg); |
| const latencyMs = Date.now() - tStart; |
| |
| |
| |
| const textBytes = utf8ByteLength(text); |
| const budget = tool._outputBudgetBytes; |
| const budgetExceeded = textBytes > budget; |
| if (telemetryEnabled()) { |
| let bytesPre: number; |
| if (jmespathUsed) { |
| |
| |
| |
| |
| |
| try { |
| const preStr = JSON.stringify(result); |
| bytesPre = utf8ByteLength(preStr === undefined ? 'null' : preStr); |
| } catch { |
| bytesPre = -1; |
| } |
| } else { |
| bytesPre = textBytes; |
| } |
| emitTelemetry('mcp.toolcall', { |
| tool: tool.name, |
| auth_kind: context.kind, |
| user_id: principalIdForLog(context), |
| latency_ms: latencyMs, |
| bytes_pre_jmespath: bytesPre, |
| bytes_post_jmespath: textBytes, |
| jmespath_used: jmespathUsed, |
| jmespath_failed: failed ?? null, |
| ok: true, |
| budget_exceeded: budgetExceeded, |
| }); |
| } |
| if (budgetExceeded) { |
| |
| |
| |
| |
| const hint = jmespathUsed |
| ? 'Response still exceeds tool output budget after JMESPath projection. Use a more selective expression to project fewer fields, or apply tool-level filters to narrow the result set.' |
| : 'Response exceeds tool output budget. Use the jmespath argument to project only the fields you need, or apply filters to narrow the result set.'; |
| return rpcOk(id, { content: [{ type: 'text', text: JSON.stringify({ |
| _budget_exceeded: true, |
| budget_bytes: budget, |
| actual_bytes: textBytes, |
| hint, |
| }) }] }, corsHeaders); |
| } |
| return rpcOk(id, { content: [{ type: 'text', text }] }, corsHeaders); |
| } catch (err: unknown) { |
| |
| |
| |
| const latencyMs = Date.now() - tStart; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const message = err instanceof Error ? err.message : String(err); |
| const isClient4xx = /HTTP 4\d\d\b/.test(message); |
| |
| |
| |
| const isExpectedDenial = err instanceof BillingDenialError; |
| const isExpectedSourceOutage = err instanceof McpSourceUnavailableError; |
| const downstreamTags = downstreamErrorTags(err); |
| const log = isClient4xx || isExpectedDenial || isExpectedSourceOutage ? console.warn : console.error; |
| log('[mcp] tool execution error:', err); |
| captureSilentError(err, { |
| tags: { |
| route: 'api/mcp', |
| step: 'tool-execution', |
| tool: tool.name, |
| auth_kind: context.kind, |
| ...(execution ? { |
| inbound_host_class: execution.inboundHostClass, |
| downstream_origin: execution.downstreamOriginTag, |
| } : {}), |
| ...downstreamTags, |
| }, |
| ctx, |
| |
| |
| fingerprint: mcpErrorFingerprint('tool-execution', tool.name, err), |
| ...(isClient4xx || isExpectedDenial || isExpectedSourceOutage ? { level: 'warning' as const } : {}), |
| }); |
| emitTelemetry('mcp.toolcall', { |
| tool: tool.name, |
| auth_kind: context.kind, |
| user_id: principalIdForLog(context), |
| latency_ms: latencyMs, |
| bytes_pre_jmespath: 0, |
| bytes_post_jmespath: 0, |
| jmespath_used: jmespathUsed, |
| jmespath_failed: null, |
| ok: false, |
| error_kind: isClient4xx |
| ? 'client_4xx' |
| : isExpectedSourceOutage |
| ? 'source_unavailable' |
| : 'server_error', |
| budget_exceeded: false, |
| }); |
| |
| |
| |
| |
| |
| if (err instanceof BillingDenialError) { |
| const denial = getMcpBillingVerificationDenial( |
| { billingStatus: err.billingCode, retryAfterSeconds: err.retryAfterSeconds }, |
| corsHeaders, |
| id, |
| ); |
| if (denial) return denial; |
| } |
| if (err instanceof McpSourceUnavailableError) { |
| return rpcError( |
| id, |
| -32003, |
| 'Required data inputs are unavailable', |
| corsHeaders, |
| { |
| retryable: true, |
| stale: true, |
| unavailable_inputs: err.unavailableInputs, |
| failed_inputs: err.failedInputs, |
| }, |
| ); |
| } |
| return rpcError(id, -32603, 'Internal error: data fetch failed', corsHeaders); |
| } |
| } |
|
|