feat: add support for ToolConfig.additionalProperties

pull/715/head
Travis Fischer 2025-06-11 12:53:23 +07:00
rodzic eb2932b799
commit 1884597812
19 zmienionych plików z 360 dodań i 59 usunięć

Wyświetl plik

@ -16,8 +16,8 @@ const fixtures = [
// 'pricing-3-plans', // 'pricing-3-plans',
// 'pricing-monthly-annual', // 'pricing-monthly-annual',
// 'pricing-custom-0', // 'pricing-custom-0',
// 'basic-openapi', 'basic-openapi',
// 'basic-mcp', 'basic-mcp',
'everything-openapi' 'everything-openapi'
] ]

Wyświetl plik

@ -221,3 +221,17 @@ et est aut quod aut provident voluptas autem voluptas",
"userId": 1, "userId": 1,
} }
`; `;
exports[`OpenAPI kitchen sink pure tool > 7.0: POST @dev/test-everything-openapi@390e70bf/pure 1`] = `
{
"foo": "bar",
"nala": "kitten",
}
`;
exports[`OpenAPI kitchen sink pure tool > 7.1: POST @dev/test-everything-openapi@390e70bf/pure 1`] = `
{
"foo": "bar",
"nala": "kitten",
}
`;

Wyświetl plik

@ -74,12 +74,6 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
) )
expect(type).toBe(expectedContentType) expect(type).toBe(expectedContentType)
if (expectedHeaders) {
for (const [key, value] of Object.entries(expectedHeaders)) {
expect(res.headers.get(key)).toBe(value)
}
}
let body: any let body: any
if (type.includes('json')) { if (type.includes('json')) {
@ -107,6 +101,12 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
expect(body).toMatchSnapshot() expect(body).toMatchSnapshot()
} }
if (expectedHeaders) {
for (const [key, value] of Object.entries(expectedHeaders)) {
expect(res.headers.get(key)).toBe(value)
}
}
if (compareResponseBodies && status >= 200 && status < 300) { if (compareResponseBodies && status >= 200 && status < 300) {
if (!fixtureResponseBody) { if (!fixtureResponseBody) {
fixtureResponseBody = body fixtureResponseBody = body

Wyświetl plik

@ -475,5 +475,54 @@ export const fixtureSuites: E2ETestFixtureSuite[] = [
} }
} }
] ]
},
{
title: 'OpenAPI kitchen sink pure tool',
sequential: true,
compareResponseBodies: true,
fixtures: [
{
path: '@dev/test-everything-openapi@390e70bf/pure',
request: {
method: 'POST',
json: {
nala: 'kitten',
foo: 'bar'
}
},
response: {
headers: {
'cache-control':
'public, max-age=31560000, s-maxage=31560000, stale-while-revalidate=3600'
},
body: {
nala: 'kitten',
foo: 'bar'
}
}
},
{
// second request should hit the cache
path: '@dev/test-everything-openapi@390e70bf/pure',
request: {
method: 'POST',
json: {
nala: 'kitten',
foo: 'bar'
}
},
response: {
headers: {
'cf-cache-status': 'HIT',
'cache-control':
'public, max-age=31560000, s-maxage=31560000, stale-while-revalidate=3600'
},
body: {
nala: 'kitten',
foo: 'bar'
}
}
}
]
} }
] ]

Wyświetl plik

@ -53,13 +53,14 @@ app.use(responseTime)
app.all(async (ctx) => { app.all(async (ctx) => {
const waitUntil = ctx.executionCtx.waitUntil.bind(ctx.executionCtx) const waitUntil = ctx.executionCtx.waitUntil.bind(ctx.executionCtx)
const isCachingEnabled = isRequestPubliclyCacheable(ctx.req.raw)
ctx.set('cache', caches.default) ctx.set('cache', caches.default)
ctx.set( ctx.set(
'client', 'client',
createAgenticClient({ createAgenticClient({
env: ctx.env, env: ctx.env,
cache: caches.default, cache: caches.default,
isCachingEnabled: isRequestPubliclyCacheable(ctx.req.raw), isCachingEnabled,
waitUntil waitUntil
}) })
) )

Wyświetl plik

@ -24,6 +24,10 @@ export async function createHttpRequestForOpenAPIOperation({
`Internal logic error for origin adapter type "${deployment.originAdapter.type}"` `Internal logic error for origin adapter type "${deployment.originAdapter.type}"`
) )
const { method } = operation
const methodHasBody =
method === 'post' || method === 'put' || method === 'patch'
// TODO: Make this more efficient by changing the `parameterSources` data structure // TODO: Make this more efficient by changing the `parameterSources` data structure
const params = Object.entries(operation.parameterSources) const params = Object.entries(operation.parameterSources)
const bodyParams = params.filter(([_key, source]) => source === 'body') const bodyParams = params.filter(([_key, source]) => source === 'body')
@ -40,6 +44,20 @@ export async function createHttpRequestForOpenAPIOperation({
'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.' 'Cookie parameters for OpenAPI operations are not yet supported. If you need cookie parameter support, please contact support@agentic.so.'
) )
// TODO: Make this more efficient...
const extraArgs = Object.keys(toolCallArgs).filter((key) => {
if (bodyParams.some(([paramKey]) => paramKey === key)) return false
if (formDataParams.some(([paramKey]) => paramKey === key)) return false
if (headerParams.some(([paramKey]) => paramKey === key)) return false
if (queryParams.some(([paramKey]) => paramKey === key)) return false
if (pathParams.some(([paramKey]) => paramKey === key)) return false
if (cookieParams.some(([paramKey]) => paramKey === key)) return false
return true
})
const extraArgsEntries = extraArgs
.map((key) => [key, toolCallArgs[key]])
.filter(([, value]) => value !== undefined)
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
if (request) { if (request) {
// TODO: do we want to expose these? especially authorization? // TODO: do we want to expose these? especially authorization?
@ -59,31 +77,40 @@ export async function createHttpRequestForOpenAPIOperation({
} }
let body: string | undefined let body: string | undefined
if (bodyParams.length > 0) { if (methodHasBody) {
body = JSON.stringify( if (bodyParams.length > 0 || !formDataParams.length) {
Object.fromEntries( const bodyJson = Object.fromEntries(
bodyParams bodyParams
.map(([key]) => [key, toolCallArgs[key]]) .map(([key]) => [key, toolCallArgs[key]])
.concat(extraArgsEntries)
// Prune undefined values. We know these aren't required fields, // Prune undefined values. We know these aren't required fields,
// because the incoming request params have already been validated // because the incoming request params have already been validated
// against the tool's input schema. // against the tool's input schema.
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
) )
)
headers['content-type'] ??= 'application/json' body = JSON.stringify(bodyJson)
} else if (formDataParams.length > 0) { headers['content-type'] = 'application/json'
// TODO: Double-check FormData usage. headers['content-length'] = body.length.toString()
const formData = new FormData() } else if (formDataParams.length > 0) {
for (const [key] of formDataParams) { // TODO: Double-check FormData usage.
const value = toolCallArgs[key] const bodyFormData = new FormData()
if (value !== undefined) {
formData.append(key, value) for (const [key] of formDataParams) {
const value = toolCallArgs[key]
if (value !== undefined) {
bodyFormData.append(key, value)
}
} }
}
body = formData.toString() for (const [key, value] of extraArgsEntries) {
headers['content-type'] ??= 'application/x-www-form-urlencoded' bodyFormData.append(key, value)
}
body = bodyFormData.toString()
headers['content-type'] = 'application/x-www-form-urlencoded'
headers['content-length'] = body.length.toString()
}
} }
let path = operation.path let path = operation.path
@ -112,13 +139,20 @@ export async function createHttpRequestForOpenAPIOperation({
for (const [key] of queryParams) { for (const [key] of queryParams) {
query.set(key, toolCallArgs[key] as string) query.set(key, toolCallArgs[key] as string)
} }
if (!methodHasBody) {
for (const [key, value] of extraArgsEntries) {
query.set(key, value)
}
}
const queryString = query.toString() const queryString = query.toString()
const originRequestUrl = `${deployment.originUrl}${path}${ const originRequestUrl = `${deployment.originUrl}${path}${
queryString ? `?${queryString}` : '' queryString ? `?${queryString}` : ''
}` }`
return new Request(originRequestUrl, { return new Request(originRequestUrl, {
method: operation.method, method: method.toUpperCase(),
body, body,
headers headers
}) })

Wyświetl plik

@ -15,26 +15,32 @@ export async function getRequestCacheKey(
return return
} }
if (request.method === 'POST' || request.method === 'PUT') { if (
request.method === 'POST' ||
request.method === 'PUT' ||
request.method === 'PATCH'
) {
const contentLength = Number.parseInt( const contentLength = Number.parseInt(
request.headers.get('content-length') ?? '0' request.headers.get('content-length') ?? '0'
) )
if (contentLength && contentLength < MAX_POST_BODY_SIZE_BYTES) { if (contentLength < MAX_POST_BODY_SIZE_BYTES) {
const { type } = contentType.safeParse( const { type } = contentType.safeParse(
request.headers.get('content-type') || 'application/octet-stream' request.headers.get('content-type') || 'application/octet-stream'
) )
let hash: string let hash = '___AGENTIC_CACHE_KEY_EMPTY_BODY___'
if (type.includes('json')) { if (contentLength > 0) {
const bodyJson: any = await request.clone().json() if (type.includes('json')) {
hash = hashObject(bodyJson) const bodyJson: any = await request.clone().json()
} else if (type.includes('text/')) { hash = hashObject(bodyJson)
const bodyString = await request.clone().text() } else if (type.includes('text/')) {
hash = await sha256(bodyString) const bodyString = await request.clone().text()
} else { hash = await sha256(bodyString)
const bodyBuffer = await request.clone().arrayBuffer() } else {
hash = await sha256(bodyBuffer) const bodyBuffer = await request.clone().arrayBuffer()
hash = await sha256(bodyBuffer)
}
} }
const cacheUrl = new URL(request.url) const cacheUrl = new URL(request.url)
@ -56,8 +62,6 @@ export async function getRequestCacheKey(
return newReq return newReq
} }
return
} else if (request.method === 'GET' || request.method === 'HEAD') { } else if (request.method === 'GET' || request.method === 'HEAD') {
const url = request.url const url = request.url
const normalizedUrl = normalizeUrl(url) const normalizedUrl = normalizeUrl(url)
@ -80,11 +84,14 @@ export async function getRequestCacheKey(
request.url, request.url,
err err
) )
return
} }
} }
const requestHeaderWhitelist = new Set(['cache-control', 'mcp-session-id']) const requestHeaderWhitelist = new Set([
'cache-control',
'content-type',
'mcp-session-id'
])
function normalizeRequestHeaders(request: Request) { function normalizeRequestHeaders(request: Request) {
const headers = Object.fromEntries(request.headers.entries()) const headers = Object.fromEntries(request.headers.entries())

Wyświetl plik

@ -34,7 +34,7 @@ export async function getToolArgsFromRequest(
data: incomingRequestArgsRaw, data: incomingRequestArgsRaw,
errorPrefix: `Invalid request parameters for tool "${tool.name}"`, errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
coerce: true, coerce: true,
strictAdditionalProperties: true strictAdditionalProperties: false
}) })
return incomingRequestArgs return incomingRequestArgs

Wyświetl plik

@ -26,7 +26,10 @@ import { fetchCache } from './fetch-cache'
import { getRequestCacheKey } from './get-request-cache-key' import { getRequestCacheKey } from './get-request-cache-key'
import { enforceRateLimit } from './rate-limits/enforce-rate-limit' import { enforceRateLimit } from './rate-limits/enforce-rate-limit'
import { updateOriginRequest } from './update-origin-request' import { updateOriginRequest } from './update-origin-request'
import { isCacheControlPubliclyCacheable } from './utils' import {
isCacheControlPubliclyCacheable,
isResponsePubliclyCacheable
} from './utils'
export async function resolveOriginToolCall({ export async function resolveOriginToolCall({
tool, tool,
@ -173,7 +176,7 @@ export async function resolveOriginToolCall({
schema: tool.inputSchema, schema: tool.inputSchema,
data: args, data: args,
errorPrefix: `Invalid request parameters for tool "${tool.name}"`, errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
strictAdditionalProperties: true strictAdditionalProperties: false
}) })
const originStartTimeMs = Date.now() const originStartTimeMs = Date.now()
@ -196,9 +199,17 @@ export async function resolveOriginToolCall({
// TODO: transform origin 5XX errors to 502 errors... // TODO: transform origin 5XX errors to 502 errors...
const originResponse = await fetchCache({ const originResponse = await fetchCache({
cacheKey, cacheKey,
fetchResponse: () => fetch(originRequest), fetchResponse: async () => {
let response = await fetch(originRequest)
if (cacheControl && isResponsePubliclyCacheable(response)) {
response = new Response(response.body, response)
response.headers.set('cache-control', cacheControl)
}
return response
},
waitUntil waitUntil
}) })
// const originResponse = await fetch(originRequest)
const cacheStatus = const cacheStatus =
(originResponse.headers.get('cf-cache-status') as CacheStatus) ?? (originResponse.headers.get('cf-cache-status') as CacheStatus) ??

Wyświetl plik

@ -64,6 +64,7 @@ test('isCacheControlPubliclyCacheable false', () => {
expect(isCacheControlPubliclyCacheable('no-store')).toBe(false) expect(isCacheControlPubliclyCacheable('no-store')).toBe(false)
expect(isCacheControlPubliclyCacheable('no-cache')).toBe(false) expect(isCacheControlPubliclyCacheable('no-cache')).toBe(false)
expect(isCacheControlPubliclyCacheable('private')).toBe(false) expect(isCacheControlPubliclyCacheable('private')).toBe(false)
expect(isCacheControlPubliclyCacheable('max-age=0')).toBe(false)
expect(isCacheControlPubliclyCacheable('private, max-age=3600')).toBe(false) expect(isCacheControlPubliclyCacheable('private, max-age=3600')).toBe(false)
expect(isCacheControlPubliclyCacheable('private, s-maxage=3600')).toBe(false) expect(isCacheControlPubliclyCacheable('private, s-maxage=3600')).toBe(false)
expect( expect(

Wyświetl plik

@ -7,6 +7,15 @@ export function isRequestPubliclyCacheable(request: Request): boolean {
return isCacheControlPubliclyCacheable(request.headers.get('cache-control')) return isCacheControlPubliclyCacheable(request.headers.get('cache-control'))
} }
export function isResponsePubliclyCacheable(response: Response): boolean {
const pragma = response.headers.get('pragma')
if (pragma === 'no-cache') {
return false
}
return isCacheControlPubliclyCacheable(response.headers.get('cache-control'))
}
export function isCacheControlPubliclyCacheable( export function isCacheControlPubliclyCacheable(
cacheControl?: string | null cacheControl?: string | null
): boolean { ): boolean {
@ -19,7 +28,8 @@ export function isCacheControlPubliclyCacheable(
if ( if (
directives.has('no-store') || directives.has('no-store') ||
directives.has('no-cache') || directives.has('no-cache') ||
directives.has('private') directives.has('private') ||
directives.has('max-age=0')
) { ) {
return false return false
} }

Wyświetl plik

@ -65,6 +65,10 @@ export default defineConfig({
{ {
name: 'disabled_rate_limit_tool', name: 'disabled_rate_limit_tool',
rateLimit: null rateLimit: null
},
{
name: 'strict_additional_properties',
additionalProperties: false
} }
] ]
}) })

Wyświetl plik

@ -0,0 +1,37 @@
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
const route = createRoute({
description: 'Echoes the request body only allowing a single "foo" field.',
operationId: 'strictAdditionalProperties',
method: 'post',
path: '/strict-additional-properties',
request: {
body: {
content: {
'application/json': {
schema: z.object({
foo: z.string()
})
}
}
}
},
responses: {
200: {
description: 'Echoed request body',
content: {
'application/json': {
schema: z.object({
foo: z.string()
})
}
}
}
}
})
export function registerStrictAdditionalProperties(app: OpenAPIHono) {
return app.openapi(route, async (c) => {
return c.json(c.req.valid('json'))
})
}

Wyświetl plik

@ -14,6 +14,7 @@ import { registerHealthCheck } from './routes/health-check'
import { registerNoCacheCacheControlTool } from './routes/no-cache-cache-control-tool' import { registerNoCacheCacheControlTool } from './routes/no-cache-cache-control-tool'
import { registerNoStoreCacheControlTool } from './routes/no-store-cache-control-tool' import { registerNoStoreCacheControlTool } from './routes/no-store-cache-control-tool'
import { registerPure } from './routes/pure' import { registerPure } from './routes/pure'
import { registerStrictAdditionalProperties } from './routes/strict-additional-properties'
import { registerUnpureMarkedPure } from './routes/unpure-marked-pure' import { registerUnpureMarkedPure } from './routes/unpure-marked-pure'
export const app = new OpenAPIHono() export const app = new OpenAPIHono()
@ -32,6 +33,7 @@ registerNoStoreCacheControlTool(app)
registerNoCacheCacheControlTool(app) registerNoCacheCacheControlTool(app)
registerCustomRateLimitTool(app) registerCustomRateLimitTool(app)
registerDisabledRateLimitTool(app) registerDisabledRateLimitTool(app)
registerStrictAdditionalProperties(app)
app.doc31('/docs', { app.doc31('/docs', {
openapi: '3.1.0', openapi: '3.1.0',

Wyświetl plik

@ -1672,6 +1672,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"path": "/health", "path": "/health",
"tags": undefined, "tags": undefined,
}, },
"no_cache_cache_control_tool": {
"method": "post",
"operationId": "noCacheCacheControlTool",
"parameterSources": {},
"path": "/no-cache-cache-control-tool",
"tags": undefined,
},
"no_store_cache_control_tool": { "no_store_cache_control_tool": {
"method": "post", "method": "post",
"operationId": "noStoreCacheControlTool", "operationId": "noStoreCacheControlTool",
@ -1686,6 +1693,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"path": "/pure", "path": "/pure",
"tags": undefined, "tags": undefined,
}, },
"unpure_marked_pure": {
"method": "post",
"operationId": "unpure_marked_pure",
"parameterSources": {},
"path": "/unpure-marked-pure",
"tags": undefined,
},
}, },
"tools": [ "tools": [
{ {
@ -1809,6 +1823,26 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"type": "object", "type": "object",
}, },
}, },
{
"description": "Unpure tool marked pure",
"inputSchema": {
"properties": {},
"required": [],
"type": "object",
},
"name": "unpure_marked_pure",
"outputSchema": {
"properties": {
"now": {
"type": "number",
},
},
"required": [
"now",
],
"type": "object",
},
},
{ {
"description": "Custom cache control tool", "description": "Custom cache control tool",
"inputSchema": { "inputSchema": {
@ -1835,6 +1869,19 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"type": "object", "type": "object",
}, },
}, },
{
"description": "No cache cache control tool",
"inputSchema": {
"properties": {},
"required": [],
"type": "object",
},
"name": "no_cache_cache_control_tool",
"outputSchema": {
"properties": {},
"type": "object",
},
},
{ {
"description": "Custom rate limit tool", "description": "Custom rate limit tool",
"inputSchema": { "inputSchema": {

Wyświetl plik

@ -22533,7 +22533,8 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
}, },
}, },
"info": { "info": {
"title": "OpenAPI server to test everything", "description": "OpenAPI kitchen sink server meant for testing Agentic's origin OpenAPI adapter and ToolConfig features.",
"title": "OpenAPI server everything",
"version": "0.1.0", "version": "0.1.0",
}, },
"openapi": "3.1.0", "openapi": "3.1.0",
@ -22732,6 +22733,35 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
}, },
}, },
}, },
"/no-cache-cache-control-tool": {
"post": {
"description": "No cache cache control tool",
"operationId": "noCacheCacheControlTool",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {},
"type": "object",
},
},
},
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {},
"type": "object",
},
},
},
"description": "Echoed request body",
},
},
},
},
"/no-store-cache-control-tool": { "/no-store-cache-control-tool": {
"post": { "post": {
"description": "No store cache control tool", "description": "No store cache control tool",
@ -22790,6 +22820,42 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
}, },
}, },
}, },
"/unpure-marked-pure": {
"post": {
"description": "Unpure tool marked pure",
"operationId": "unpure_marked_pure",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {},
"type": "object",
},
},
},
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"now": {
"type": "number",
},
},
"required": [
"now",
],
"type": "object",
},
},
},
"description": "Echoed request body with current timestamp to not be pure",
},
},
},
},
"/users/{userId}": { "/users/{userId}": {
"get": { "get": {
"description": "Gets a user", "description": "Gets a user",

Wyświetl plik

@ -135,7 +135,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
"name": "test-everything-openapi", "name": "test-everything-openapi",
"originAdapter": { "originAdapter": {
"location": "external", "location": "external",
"spec": "{"openapi":"3.1.0","info":{"title":"OpenAPI server to test everything","version":"0.1.0"},"components":{"schemas":{"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"]}},"parameters":{}},"paths":{"/health":{"get":{"description":"Check if the server is healthy","operationId":"healthCheck","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/users/{userId}":{"get":{"description":"Gets a user","tags":["users"],"operationId":"getUser","parameters":[{"schema":{"type":"string"},"required":true,"description":"User ID","name":"userId","in":"path"}],"responses":{"200":{"description":"A user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/disabled-tool":{"get":{"description":"Disabled tool","operationId":"disabledTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/disabled-for-free-plan-tool":{"get":{"description":"Disabled for free plan tool","operationId":"disabledForFreePlanTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/echo":{"post":{"description":"Echoes the request body","operationId":"echo","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/pure":{"post":{"description":"Pure tool","operationId":"pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-cache-control-tool":{"post":{"description":"Custom cache control tool","operationId":"customCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-store-cache-control-tool":{"post":{"description":"No store cache control tool","operationId":"noStoreCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-rate-limit-tool":{"post":{"description":"Custom rate limit tool","operationId":"customRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/disabled-rate-limit-tool":{"post":{"description":"Disabled rate limit tool","operationId":"disabledRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}},"webhooks":{}}", "spec": "{"openapi":"3.1.0","info":{"title":"OpenAPI server everything","description":"OpenAPI kitchen sink server meant for testing Agentic's origin OpenAPI adapter and ToolConfig features.","version":"0.1.0"},"components":{"schemas":{"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"]}},"parameters":{}},"paths":{"/health":{"get":{"description":"Check if the server is healthy","operationId":"healthCheck","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/users/{userId}":{"get":{"description":"Gets a user","tags":["users"],"operationId":"getUser","parameters":[{"schema":{"type":"string"},"required":true,"description":"User ID","name":"userId","in":"path"}],"responses":{"200":{"description":"A user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/disabled-tool":{"get":{"description":"Disabled tool","operationId":"disabledTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/disabled-for-free-plan-tool":{"get":{"description":"Disabled for free plan tool","operationId":"disabledForFreePlanTool","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]}}}}}}},"/echo":{"post":{"description":"Echoes the request body","operationId":"echo","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/pure":{"post":{"description":"Pure tool","operationId":"pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/unpure-marked-pure":{"post":{"description":"Unpure tool marked pure","operationId":"unpure_marked_pure","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body with current timestamp to not be pure","content":{"application/json":{"schema":{"type":"object","properties":{"now":{"type":"number"}},"required":["now"]}}}}}}},"/custom-cache-control-tool":{"post":{"description":"Custom cache control tool","operationId":"customCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-store-cache-control-tool":{"post":{"description":"No store cache control tool","operationId":"noStoreCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/no-cache-cache-control-tool":{"post":{"description":"No cache cache control tool","operationId":"noCacheCacheControlTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/custom-rate-limit-tool":{"post":{"description":"Custom rate limit tool","operationId":"customRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/disabled-rate-limit-tool":{"post":{"description":"Disabled rate limit tool","operationId":"disabledRateLimitTool","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Echoed request body","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}},"webhooks":{}}",
"type": "openapi", "type": "openapi",
}, },
"originUrl": "https://agentic-platform-fixtures-everything.onrender.com", "originUrl": "https://agentic-platform-fixtures-everything.onrender.com",
@ -158,7 +158,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
"toolConfigs": [ "toolConfigs": [
{ {
"enabled": true, "enabled": true,
"name": "getUser", "name": "get_user",
"pricingPlanOverridesMap": { "pricingPlanOverridesMap": {
"free": { "free": {
"enabled": true, "enabled": true,
@ -171,13 +171,13 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
}, },
{ {
"enabled": false, "enabled": false,
"name": "disabledTool", "name": "disabled_tool",
"pure": false, "pure": false,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"enabled": true, "enabled": true,
"name": "disabledForFreePlanTool", "name": "disabled_for_free_plan_tool",
"pricingPlanOverridesMap": { "pricingPlanOverridesMap": {
"free": { "free": {
"enabled": false, "enabled": false,
@ -188,40 +188,40 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
}, },
{ {
"enabled": true, "enabled": true,
"name": "pureTool", "name": "pure",
"pure": true, "pure": true,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"enabled": true, "enabled": true,
"name": "unpureToolMarkedPure", "name": "unpure_marked_pure",
"pure": true, "pure": true,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"cacheControl": "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600", "cacheControl": "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600",
"enabled": true, "enabled": true,
"name": "customCacheControlTool", "name": "custom_cache_control_tool",
"pure": false, "pure": false,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"cacheControl": "no-cache", "cacheControl": "no-cache",
"enabled": true, "enabled": true,
"name": "noCacheCacheControlTool", "name": "no_cache_cache_control_tool",
"pure": false, "pure": false,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"cacheControl": "no-store", "cacheControl": "no-store",
"enabled": true, "enabled": true,
"name": "noStoreCacheControlTool", "name": "no_store_cache_control_tool",
"pure": false, "pure": false,
"reportUsage": true, "reportUsage": true,
}, },
{ {
"enabled": true, "enabled": true,
"name": "customRateLimitTool", "name": "custom_rate_limit_tool",
"pure": false, "pure": false,
"rateLimit": { "rateLimit": {
"async": true, "async": true,
@ -232,7 +232,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
}, },
{ {
"enabled": true, "enabled": true,
"name": "disabledRateLimitTool", "name": "disabled_rate_limit_tool",
"pure": false, "pure": false,
"rateLimit": null, "rateLimit": null,
"reportUsage": true, "reportUsage": true,

Wyświetl plik

@ -460,6 +460,9 @@ export interface components {
[key: string]: unknown; [key: string]: unknown;
}; };
required?: string[]; required?: string[];
additionalProperties?: boolean | {
[key: string]: unknown;
};
}; };
Tool: { Tool: {
/** @description Agentic tool name */ /** @description Agentic tool name */
@ -502,6 +505,8 @@ export interface components {
/** @default true */ /** @default true */
reportUsage: boolean; reportUsage: boolean;
rateLimit?: components["schemas"]["RateLimit"] | null; rateLimit?: components["schemas"]["RateLimit"] | null;
/** @default true */
additionalProperties: boolean;
/** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */ /** @description Allows you to override this tool's behavior or disable it entirely for different pricing plans. This is a map of PricingPlan slug to PricingPlanToolOverrides for that plan. */
pricingPlanOverridesMap?: { pricingPlanOverridesMap?: {
[key: string]: components["schemas"]["PricingPlanToolOverride"]; [key: string]: components["schemas"]["PricingPlanToolOverride"];

Wyświetl plik

@ -36,7 +36,10 @@ export const jsonSchemaObjectSchema = z
type: z.literal('object'), type: z.literal('object'),
// TODO: improve this schema // TODO: improve this schema
properties: z.record(z.string(), z.any()).optional(), properties: z.record(z.string(), z.any()).optional(),
required: z.array(z.string()).optional() required: z.array(z.string()).optional(),
additionalProperties: z
.union([z.boolean(), z.record(z.string(), z.any())])
.optional()
}) })
.passthrough() .passthrough()
.openapi('JsonSchemaObject') .openapi('JsonSchemaObject')
@ -157,6 +160,16 @@ export const toolConfigSchema = z
*/ */
rateLimit: z.union([rateLimitSchema, z.null()]).optional(), rateLimit: z.union([rateLimitSchema, z.null()]).optional(),
/**
* Whether to allow additional properties in the tool's input schema.
*
* The default MCP spec allows additional properties. Set this to `false` if
* you want your tool to be more strict.
*
* @default true
*/
additionalProperties: z.boolean().optional().default(true),
/** /**
* Allows you to override this tool's behavior or disable it entirely for * Allows you to override this tool's behavior or disable it entirely for
* different pricing plans. * different pricing plans.