Fix Gemini schema validation: ARRAY needs items, replace free OBJECT with JSON strings
Gemini's function_declarations validator requires: - ARRAY types must declare items schema - Free-form OBJECT (without properties) is rejected Renamed free-object params to *Json string fields (envsJson, patchJson, headersJson) and added server-side JSON.parse before forwarding to MCP. Any param ending in "Json" is automatically unpacked into its real key (e.g. envsJson string is parsed into envs object). Made-with: Cursor
This commit is contained in:
@@ -82,7 +82,7 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
composeRaw: { type: 'STRING', description: 'Raw Docker Compose YAML for custom multi-service stacks. Only use when no template exists.' },
|
||||
repo: { type: 'STRING', description: 'Gitea repo name (e.g. "my-site") for deploying the user\'s own code.' },
|
||||
ports: { type: 'STRING', description: 'Port(s) the app listens on (e.g. "3000"). Required for repo/image pathways.' },
|
||||
envs: { type: 'OBJECT', description: 'Environment variables as key-value pairs to inject at creation.' },
|
||||
envsJson: { type: 'STRING', description: 'Environment variables as a JSON object string (e.g. \'{"KEY":"value"}\'). Optional.' },
|
||||
instantDeploy: { type: 'BOOLEAN', description: 'Whether to deploy immediately (default true).' },
|
||||
},
|
||||
required: ['name'],
|
||||
@@ -95,9 +95,9 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
uuid: { type: 'STRING', description: 'The Coolify application UUID.' },
|
||||
patch: { type: 'OBJECT', description: 'Fields to update as key-value pairs.' },
|
||||
patchJson: { type: 'STRING', description: 'Fields to update as a JSON object string (e.g. \'{"name":"new-name","ports_exposes":"3001"}\').' },
|
||||
},
|
||||
required: ['uuid', 'patch'],
|
||||
required: ['uuid', 'patchJson'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -281,7 +281,11 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
uuid: { type: 'STRING', description: 'The Coolify application UUID.' },
|
||||
domains: { type: 'ARRAY', description: 'Array of domain strings (e.g. ["myapp.mark.vibnai.com", "api.mark.vibnai.com"]).' },
|
||||
domains: {
|
||||
type: 'ARRAY',
|
||||
description: 'Array of domain strings (e.g. ["myapp.mark.vibnai.com", "api.mark.vibnai.com"]).',
|
||||
items: { type: 'STRING' },
|
||||
},
|
||||
service: { type: 'STRING', description: 'For compose apps: the service to attach the domain to (e.g. "server"). Default: "server".' },
|
||||
},
|
||||
required: ['uuid', 'domains'],
|
||||
@@ -365,9 +369,9 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
uuid: { type: 'STRING', description: 'The Coolify database UUID.' },
|
||||
patch: { type: 'OBJECT', description: 'Fields to update.' },
|
||||
patchJson: { type: 'STRING', description: 'Fields to update as a JSON object string (e.g. \'{"name":"new-name","is_public":true}\').' },
|
||||
},
|
||||
required: ['uuid', 'patch'],
|
||||
required: ['uuid', 'patchJson'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -425,7 +429,11 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
names: { type: 'ARRAY', description: 'Array of domain names to check (e.g. ["myapp.com", "myapp.io"]). Max 25.' },
|
||||
names: {
|
||||
type: 'ARRAY',
|
||||
description: 'Array of domain names to check (e.g. ["myapp.com", "myapp.io"]). Max 25.',
|
||||
items: { type: 'STRING' },
|
||||
},
|
||||
period: { type: 'NUMBER', description: 'Registration period in years (default 1). Note: .ai requires 2 years minimum.' },
|
||||
},
|
||||
required: ['names'],
|
||||
@@ -468,7 +476,11 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
properties: {
|
||||
domain: { type: 'STRING', description: 'The registered domain name (e.g. "myapp.com").' },
|
||||
appUuid: { type: 'STRING', description: 'Coolify app UUID to attach the domain to.' },
|
||||
subdomains: { type: 'ARRAY', description: 'Subdomains to wire (default ["@", "www"]).' },
|
||||
subdomains: {
|
||||
type: 'ARRAY',
|
||||
description: 'Subdomains to wire (default ["@", "www"]).',
|
||||
items: { type: 'STRING' },
|
||||
},
|
||||
},
|
||||
required: ['domain'],
|
||||
},
|
||||
@@ -545,7 +557,7 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
url: { type: 'STRING', description: 'The full URL to fetch (https preferred).' },
|
||||
headers: { type: 'OBJECT', description: 'Optional HTTP headers as key-value pairs.' },
|
||||
headersJson: { type: 'STRING', description: 'Optional HTTP headers as a JSON object string (e.g. \'{"Accept":"application/json"}\').' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
@@ -574,6 +586,21 @@ export async function executeMcpTool(
|
||||
// Convert underscore tool name → dotted MCP action (apps_create → apps.create)
|
||||
const action = toolName.replace(/_/g, '.');
|
||||
|
||||
// Unpack JSON-string args (Gemini schemas can't represent free-form objects,
|
||||
// so we accept *Json string fields and parse them server-side).
|
||||
const params: Record<string, unknown> = { ...args };
|
||||
for (const key of Object.keys(params)) {
|
||||
if (key.endsWith('Json') && typeof params[key] === 'string') {
|
||||
const realKey = key.slice(0, -4); // envsJson → envs, patchJson → patch
|
||||
try {
|
||||
params[realKey] = JSON.parse(params[key] as string);
|
||||
} catch {
|
||||
return JSON.stringify({ error: `Invalid JSON for ${key}` });
|
||||
}
|
||||
delete params[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/mcp`, {
|
||||
method: 'POST',
|
||||
@@ -581,7 +608,7 @@ export async function executeMcpTool(
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${mcpToken}`,
|
||||
},
|
||||
body: JSON.stringify({ action, params: args }),
|
||||
body: JSON.stringify({ action, params }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return JSON.stringify(data.result ?? data.error ?? data, null, 2).slice(0, 8000);
|
||||
@@ -655,7 +682,10 @@ async function executeGithubFile(args: Record<string, unknown>): Promise<string>
|
||||
|
||||
async function executeHttpFetch(args: Record<string, unknown>): Promise<string> {
|
||||
const url = String(args.url || '');
|
||||
const extraHeaders = (args.headers as Record<string, string>) || {};
|
||||
let extraHeaders: Record<string, string> = {};
|
||||
if (typeof args.headersJson === 'string') {
|
||||
try { extraHeaders = JSON.parse(args.headersJson); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
return JSON.stringify({ error: 'URL must start with http:// or https://' });
|
||||
|
||||
Reference in New Issue
Block a user