fix: send an endpoint's hidden body constants from the chat API executors too

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DP2b87ZiA4EcXSVzTg2ex
This commit is contained in:
Ruben Fiszel
2026-08-20 21:29:38 +00:00
co-authored by Claude Opus 5
parent 760000deab
commit 421024b506
6 changed files with 70 additions and 9 deletions
@@ -109,6 +109,9 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
if param_required:
query_params_schema['required'].append(param_name)
if body_constants and not request_body:
raise ValueError("x-mcp-tool-body-constants needs a request body to set them on")
# Process request body if present
if request_body:
body_schema = extract_request_body_schema(request_body, spec, base_path)
+4
View File
@@ -26102,6 +26102,10 @@ components:
type: object
description: JSON schema for request body
nullable: true
body_constants:
type: object
description: Body fields the caller must send on top of the ones it assembles from body_schema, which does not describe them
nullable: true
AIProvider:
type: string
@@ -4,6 +4,7 @@ import { get } from 'svelte/store'
import { workspaceStore } from '$lib/stores'
import type { EndpointTool } from '$lib/gen/types.gen'
import { McpService } from '$lib/gen/services.gen'
import { withBodyConstants } from './bodyConstants'
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionFunctionTool {
// Build the parameters schema for OpenAI function calling
@@ -74,20 +75,17 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionFunctionToo
function buildToolsFromEndpoints(endpointTools: EndpointTool[]): {
tools: ChatCompletionFunctionTool[]
endpointMap: Record<string, { method: string; path: string }>
endpointMap: Record<string, EndpointTool>
} {
const tools: ChatCompletionFunctionTool[] = []
const endpointMap: Record<string, { method: string; path: string }> = {}
const endpointMap: Record<string, EndpointTool> = {}
for (const endpointTool of endpointTools) {
const tool = buildApiCallTool(endpointTool)
tools.push(tool)
// Store the endpoint info in the map
endpointMap[endpointTool.name] = {
method: endpointTool.method,
path: endpointTool.path
}
endpointMap[endpointTool.name] = endpointTool
}
return { tools, endpointMap }
@@ -95,7 +93,7 @@ function buildToolsFromEndpoints(endpointTools: EndpointTool[]): {
export function createApiTools(
chatTools: ChatCompletionFunctionTool[],
endpointMap: Record<string, { method: string; path: string }> = {}
endpointMap: Record<string, EndpointTool> = {}
): Tool<{}>[] {
return chatTools.map((chatTool) => {
const toolName = chatTool.function.name
@@ -167,7 +165,7 @@ export function createApiTools(
fetchOptions.headers = {
'Content-Type': 'application/json'
}
fetchOptions.body = JSON.stringify(requestBody)
fetchOptions.body = JSON.stringify(withBodyConstants(endpoint, requestBody))
}
const response = await fetch(url, fetchOptions)
@@ -0,0 +1,20 @@
import type { EndpointTool } from '$lib/gen/types.gen'
/**
* Add the body fields an endpoint fills in for its callers rather than exposing.
* They are deliberately absent from `body_schema` — a field whose only correct
* value is fixed invites a model to place a placeholder there — so a body
* assembled from that schema alone omits them and the call reaches the API
* incomplete. Mirrors what the MCP server does for its own callers.
*/
export function withBodyConstants(endpoint: EndpointTool, body: unknown): unknown {
if (
!endpoint.body_constants ||
typeof body !== 'object' ||
body === null ||
Array.isArray(body)
) {
return body
}
return { ...body, ...endpoint.body_constants }
}
@@ -103,6 +103,23 @@ const CATALOG = [
type: 'object',
properties: { args: { type: 'object' } }
}
},
{
name: 'runScriptByPath',
description: 'Run script by path',
instructions: '',
path: '/w/{workspace}/jobs/run/p/{path}',
method: 'POST',
path_params_schema: {
type: 'object',
properties: { workspace: { type: 'string' }, path: { type: 'string' } },
required: ['workspace', 'path']
},
body_schema: {
type: 'object',
properties: { args: { type: 'object' } }
},
body_constants: { invisible_flag: true }
}
]
@@ -245,6 +262,24 @@ describe('call_api_endpoint', () => {
expect(result).toEqual({ success: true, data: 'job-id-1' })
})
it('sends the body fields the endpoint keeps out of its schema', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
headers: new Headers({ 'content-type': 'text/plain' }),
text: async () => 'job-id-2'
})
vi.stubGlobal('fetch', fetchMock)
await run('call_api_endpoint', {
name: 'runScriptByPath',
params: { path: 'u/me/myscript' },
body: { args: { n: 1 } }
})
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({
args: { n: 1 },
invisible_flag: true
})
})
it('redirects GET endpoints to call_api_get', async () => {
const result = await run('call_api_endpoint', { name: 'listWorkers' })
expect(result.error).toContain('call_api_get')
@@ -1,6 +1,7 @@
import { z } from 'zod'
import { McpService, type EndpointTool } from '$lib/gen'
import { createToolDef, type Tool } from '../shared'
import { withBodyConstants } from '../api/bodyConstants'
/**
* Generic access to the backend's MCP endpoint catalog (the endpoints marked
@@ -195,7 +196,7 @@ async function executeEndpoint(
const fetchOptions: RequestInit = { method }
if (body !== undefined && method !== 'GET') {
fetchOptions.headers = { 'Content-Type': 'application/json' }
fetchOptions.body = JSON.stringify(body)
fetchOptions.body = JSON.stringify(withBodyConstants(endpoint, body))
}
const response = await fetch(url, fetchOptions)