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-monthly-annual',
// 'pricing-custom-0',
// 'basic-openapi',
// 'basic-mcp',
'basic-openapi',
'basic-mcp',
'everything-openapi'
]

Wyświetl plik

@ -221,3 +221,17 @@ et est aut quod aut provident voluptas autem voluptas",
"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)
if (expectedHeaders) {
for (const [key, value] of Object.entries(expectedHeaders)) {
expect(res.headers.get(key)).toBe(value)
}
}
let body: any
if (type.includes('json')) {
@ -107,6 +101,12 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
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 (!fixtureResponseBody) {
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) => {
const waitUntil = ctx.executionCtx.waitUntil.bind(ctx.executionCtx)
const isCachingEnabled = isRequestPubliclyCacheable(ctx.req.raw)
ctx.set('cache', caches.default)
ctx.set(
'client',
createAgenticClient({
env: ctx.env,
cache: caches.default,
isCachingEnabled: isRequestPubliclyCacheable(ctx.req.raw),
isCachingEnabled,
waitUntil
})
)

Wyświetl plik

@ -24,6 +24,10 @@ export async function createHttpRequestForOpenAPIOperation({
`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
const params = Object.entries(operation.parameterSources)
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.'
)
// 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> = {}
if (request) {
// TODO: do we want to expose these? especially authorization?
@ -59,31 +77,40 @@ export async function createHttpRequestForOpenAPIOperation({
}
let body: string | undefined
if (bodyParams.length > 0) {
body = JSON.stringify(
Object.fromEntries(
if (methodHasBody) {
if (bodyParams.length > 0 || !formDataParams.length) {
const bodyJson = Object.fromEntries(
bodyParams
.map(([key]) => [key, toolCallArgs[key]])
.concat(extraArgsEntries)
// Prune undefined values. We know these aren't required fields,
// because the incoming request params have already been validated
// against the tool's input schema.
.filter(([, value]) => value !== undefined)
)
)
headers['content-type'] ??= 'application/json'
} else if (formDataParams.length > 0) {
// TODO: Double-check FormData usage.
const formData = new FormData()
for (const [key] of formDataParams) {
const value = toolCallArgs[key]
if (value !== undefined) {
formData.append(key, value)
body = JSON.stringify(bodyJson)
headers['content-type'] = 'application/json'
headers['content-length'] = body.length.toString()
} else if (formDataParams.length > 0) {
// TODO: Double-check FormData usage.
const bodyFormData = new FormData()
for (const [key] of formDataParams) {
const value = toolCallArgs[key]
if (value !== undefined) {
bodyFormData.append(key, value)
}
}
}
body = formData.toString()
headers['content-type'] ??= 'application/x-www-form-urlencoded'
for (const [key, value] of extraArgsEntries) {
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
@ -112,13 +139,20 @@ export async function createHttpRequestForOpenAPIOperation({
for (const [key] of queryParams) {
query.set(key, toolCallArgs[key] as string)
}
if (!methodHasBody) {
for (const [key, value] of extraArgsEntries) {
query.set(key, value)
}
}
const queryString = query.toString()
const originRequestUrl = `${deployment.originUrl}${path}${
queryString ? `?${queryString}` : ''
}`
return new Request(originRequestUrl, {
method: operation.method,
method: method.toUpperCase(),
body,
headers
})

Wyświetl plik

@ -15,26 +15,32 @@ export async function getRequestCacheKey(
return
}
if (request.method === 'POST' || request.method === 'PUT') {
if (
request.method === 'POST' ||
request.method === 'PUT' ||
request.method === 'PATCH'
) {
const contentLength = Number.parseInt(
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(
request.headers.get('content-type') || 'application/octet-stream'
)
let hash: string
let hash = '___AGENTIC_CACHE_KEY_EMPTY_BODY___'
if (type.includes('json')) {
const bodyJson: any = await request.clone().json()
hash = hashObject(bodyJson)
} else if (type.includes('text/')) {
const bodyString = await request.clone().text()
hash = await sha256(bodyString)
} else {
const bodyBuffer = await request.clone().arrayBuffer()
hash = await sha256(bodyBuffer)
if (contentLength > 0) {
if (type.includes('json')) {
const bodyJson: any = await request.clone().json()
hash = hashObject(bodyJson)
} else if (type.includes('text/')) {
const bodyString = await request.clone().text()
hash = await sha256(bodyString)
} else {
const bodyBuffer = await request.clone().arrayBuffer()
hash = await sha256(bodyBuffer)
}
}
const cacheUrl = new URL(request.url)
@ -56,8 +62,6 @@ export async function getRequestCacheKey(
return newReq
}
return
} else if (request.method === 'GET' || request.method === 'HEAD') {
const url = request.url
const normalizedUrl = normalizeUrl(url)
@ -80,11 +84,14 @@ export async function getRequestCacheKey(
request.url,
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) {
const headers = Object.fromEntries(request.headers.entries())

Wyświetl plik

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

Wyświetl plik

@ -26,7 +26,10 @@ import { fetchCache } from './fetch-cache'
import { getRequestCacheKey } from './get-request-cache-key'
import { enforceRateLimit } from './rate-limits/enforce-rate-limit'
import { updateOriginRequest } from './update-origin-request'
import { isCacheControlPubliclyCacheable } from './utils'
import {
isCacheControlPubliclyCacheable,
isResponsePubliclyCacheable
} from './utils'
export async function resolveOriginToolCall({
tool,
@ -173,7 +176,7 @@ export async function resolveOriginToolCall({
schema: tool.inputSchema,
data: args,
errorPrefix: `Invalid request parameters for tool "${tool.name}"`,
strictAdditionalProperties: true
strictAdditionalProperties: false
})
const originStartTimeMs = Date.now()
@ -196,9 +199,17 @@ export async function resolveOriginToolCall({
// TODO: transform origin 5XX errors to 502 errors...
const originResponse = await fetchCache({
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
})
// const originResponse = await fetch(originRequest)
const 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-cache')).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, s-maxage=3600')).toBe(false)
expect(

Wyświetl plik

@ -7,6 +7,15 @@ export function isRequestPubliclyCacheable(request: Request): boolean {
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(
cacheControl?: string | null
): boolean {
@ -19,7 +28,8 @@ export function isCacheControlPubliclyCacheable(
if (
directives.has('no-store') ||
directives.has('no-cache') ||
directives.has('private')
directives.has('private') ||
directives.has('max-age=0')
) {
return false
}

Wyświetl plik

@ -65,6 +65,10 @@ export default defineConfig({
{
name: 'disabled_rate_limit_tool',
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 { registerNoStoreCacheControlTool } from './routes/no-store-cache-control-tool'
import { registerPure } from './routes/pure'
import { registerStrictAdditionalProperties } from './routes/strict-additional-properties'
import { registerUnpureMarkedPure } from './routes/unpure-marked-pure'
export const app = new OpenAPIHono()
@ -32,6 +33,7 @@ registerNoStoreCacheControlTool(app)
registerNoCacheCacheControlTool(app)
registerCustomRateLimitTool(app)
registerDisabledRateLimitTool(app)
registerStrictAdditionalProperties(app)
app.doc31('/docs', {
openapi: '3.1.0',

Wyświetl plik

@ -1672,6 +1672,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"path": "/health",
"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": {
"method": "post",
"operationId": "noStoreCacheControlTool",
@ -1686,6 +1693,13 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"path": "/pure",
"tags": undefined,
},
"unpure_marked_pure": {
"method": "post",
"operationId": "unpure_marked_pure",
"parameterSources": {},
"path": "/unpure-marked-pure",
"tags": undefined,
},
},
"tools": [
{
@ -1809,6 +1823,26 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"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",
"inputSchema": {
@ -1835,6 +1869,19 @@ exports[`getToolsFromOpenAPISpec > remote spec https://agentic-platform-fixtures
"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",
"inputSchema": {

Wyświetl plik

@ -22533,7 +22533,8 @@ exports[`validateOpenAPISpec > remote spec https://agentic-platform-fixtures-eve
},
},
"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",
},
"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": {
"post": {
"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}": {
"get": {
"description": "Gets a user",

Wyświetl plik

@ -135,7 +135,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
"name": "test-everything-openapi",
"originAdapter": {
"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",
},
"originUrl": "https://agentic-platform-fixtures-everything.onrender.com",
@ -158,7 +158,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
"toolConfigs": [
{
"enabled": true,
"name": "getUser",
"name": "get_user",
"pricingPlanOverridesMap": {
"free": {
"enabled": true,
@ -171,13 +171,13 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
},
{
"enabled": false,
"name": "disabledTool",
"name": "disabled_tool",
"pure": false,
"reportUsage": true,
},
{
"enabled": true,
"name": "disabledForFreePlanTool",
"name": "disabled_for_free_plan_tool",
"pricingPlanOverridesMap": {
"free": {
"enabled": false,
@ -188,40 +188,40 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
},
{
"enabled": true,
"name": "pureTool",
"name": "pure",
"pure": true,
"reportUsage": true,
},
{
"enabled": true,
"name": "unpureToolMarkedPure",
"name": "unpure_marked_pure",
"pure": true,
"reportUsage": true,
},
{
"cacheControl": "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600",
"enabled": true,
"name": "customCacheControlTool",
"name": "custom_cache_control_tool",
"pure": false,
"reportUsage": true,
},
{
"cacheControl": "no-cache",
"enabled": true,
"name": "noCacheCacheControlTool",
"name": "no_cache_cache_control_tool",
"pure": false,
"reportUsage": true,
},
{
"cacheControl": "no-store",
"enabled": true,
"name": "noStoreCacheControlTool",
"name": "no_store_cache_control_tool",
"pure": false,
"reportUsage": true,
},
{
"enabled": true,
"name": "customRateLimitTool",
"name": "custom_rate_limit_tool",
"pure": false,
"rateLimit": {
"async": true,
@ -232,7 +232,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
},
{
"enabled": true,
"name": "disabledRateLimitTool",
"name": "disabled_rate_limit_tool",
"pure": false,
"rateLimit": null,
"reportUsage": true,

Wyświetl plik

@ -460,6 +460,9 @@ export interface components {
[key: string]: unknown;
};
required?: string[];
additionalProperties?: boolean | {
[key: string]: unknown;
};
};
Tool: {
/** @description Agentic tool name */
@ -502,6 +505,8 @@ export interface components {
/** @default true */
reportUsage: boolean;
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. */
pricingPlanOverridesMap?: {
[key: string]: components["schemas"]["PricingPlanToolOverride"];

Wyświetl plik

@ -36,7 +36,10 @@ export const jsonSchemaObjectSchema = z
type: z.literal('object'),
// TODO: improve this schema
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()
.openapi('JsonSchemaObject')
@ -157,6 +160,16 @@ export const toolConfigSchema = z
*/
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
* different pricing plans.