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:
2026-04-27 18:02:03 -07:00
parent c4ef30066f
commit 95ab91727e

View File

@@ -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://' });