mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 00:02:35 +00:00
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:
co-authored by
Claude Opus 5
parent
760000deab
commit
421024b506
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user