Skip to content

Commit ab47ff2

Browse files
Merge pull request #33 from freddy-schuetz/feature/mcp-bridge-schema-retry
Schema-hint retry for external MCP tool calls
2 parents 0868ebf + b0731e6 commit ab47ff2

3 files changed

Lines changed: 84 additions & 24 deletions

File tree

workflows/background-checker.json

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@
101101
"name": "Start",
102102
"type": "n8n-nodes-base.executeWorkflowTrigger",
103103
"typeVersion": 1.1,
104-
"position": [0, 0],
104+
"position": [
105+
0,
106+
0
107+
],
105108
"parameters": {
106109
"inputSource": "passthrough"
107110
}
@@ -111,7 +114,10 @@
111114
"name": "Build Prompt",
112115
"type": "n8n-nodes-base.code",
113116
"typeVersion": 2,
114-
"position": [220, 0],
117+
"position": [
118+
220,
119+
0
120+
],
115121
"parameters": {
116122
"jsCode": "const raw = $input.first().json;\nconst instruction = raw.instruction || raw.message || '';\nconst mcpSkills = raw.mcp_skills || '';\nconst lang = raw.language || 'German';\nconst lastRun = raw.last_run || null;\n\nconst now = new Date().toLocaleString('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'full', timeStyle: 'short' });\n\nlet timeContext = '';\nif (lastRun) {\n const lastRunDate = new Date(lastRun);\n const lastRunStr = lastRunDate.toLocaleString('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'full', timeStyle: 'short' });\n timeContext = `\\nIMPORTANT: Your last check was at ${lastRunStr}. ONLY report things that happened or arrived AFTER this time. Anything from before that time has already been reported — ignore it completely.\\nIf there is nothing new since ${lastRunStr}, respond [SILENT].`;\n} else {\n timeContext = `\\nThis is the FIRST check. Report current state briefly if there is anything noteworthy.`;\n}\n\nconst systemPrompt = `# BACKGROUND CHECKER\nCurrent time: ${now}\n\nYou are a background checker executing a scheduled task silently.\nExecute the instruction below using the available tools.\n${timeContext}\n\nRULES:\n- ONLY use the tool(s) needed for the instruction. The instruction tells you which tool to use.\n- Do NOT call tools on unrelated MCP servers. Do NOT explore or discover all available tools.\n- Call the MCP Client ONCE with the correct server URL and tool name from the list below.\n- NEVER send messages yourself (no Telegram, no notifications). Just return your findings as text.\n- If the result shows something NEW (after your last check), describe it concisely in ${lang}.\n- If there is NOTHING new or changed to report, respond with ONLY the exact text: [SILENT]\n- Do NOT add greetings, commentary, or explanations when responding [SILENT].\n- Ignore any part of the instruction that asks you to send notifications or messages — just report findings.\n- Be concise when reporting changes — just the facts.\n- Maximum 3 tool calls total. If you cannot complete the task in 3 calls, respond [SILENT].\n\n${mcpSkills ? '# AVAILABLE MCP SKILLS (reference only — use ONLY what the instruction needs)\\n' + mcpSkills : ''}`;\n\nreturn [{\n json: {\n systemPrompt,\n userMessage: instruction\n }\n}];"
117123
}
@@ -121,7 +127,10 @@
121127
"name": "Checker Agent",
122128
"type": "@n8n/n8n-nodes-langchain.agent",
123129
"typeVersion": 3.1,
124-
"position": [480, 0],
130+
"position": [
131+
480,
132+
0
133+
],
125134
"parameters": {
126135
"promptType": "define",
127136
"text": "={{ $json.userMessage }}",
@@ -136,7 +145,10 @@
136145
"name": "Claude",
137146
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
138147
"typeVersion": 1.3,
139-
"position": [352, 240],
148+
"position": [
149+
352,
150+
240
151+
],
140152
"parameters": {
141153
"model": {
142154
"__rl": true,
@@ -161,7 +173,10 @@
161173
"name": "HTTP Request Tool",
162174
"type": "@n8n/n8n-nodes-langchain.toolCode",
163175
"typeVersion": 1.2,
164-
"position": [480, 240],
176+
"position": [
177+
480,
178+
240
179+
],
165180
"parameters": {
166181
"description": "Make HTTP requests to any URL. Input: a JSON object with 'url' (required), 'method' (GET/POST/PUT/DELETE, default GET), 'headers' (object, optional), 'body' (string or object, optional).",
167182
"jsCode": "const qType = typeof query;\nlet url = '', method = 'GET', headers = {}, body = null;\n\nif (qType === 'object' && query !== null) {\n url = query.url || '';\n method = (query.method || 'GET').toUpperCase();\n headers = query.headers || {};\n body = query.body || null;\n} else if (qType === 'string') {\n try {\n const parsed = JSON.parse(query);\n url = parsed.url || '';\n method = (parsed.method || 'GET').toUpperCase();\n headers = parsed.headers || {};\n body = parsed.body || null;\n } catch(e) {\n url = query;\n }\n}\n\nif (!url) return 'Error: no URL provided';\n\ntry {\n const opts = { method, url, headers };\n if (body && method !== 'GET') {\n opts.body = typeof body === 'string' ? body : JSON.stringify(body);\n if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';\n }\n const resp = await helpers.httpRequest(opts);\n const result = typeof resp === 'string' ? resp : JSON.stringify(resp);\n return result.length > 10000 ? result.substring(0, 10000) + '... (truncated)' : result;\n} catch(e) {\n return 'HTTP Error: ' + e.message;\n}",
@@ -173,7 +188,10 @@
173188
"name": "Web Search",
174189
"type": "@n8n/n8n-nodes-langchain.toolCode",
175190
"typeVersion": 1.2,
176-
"position": [608, 240],
191+
"position": [
192+
608,
193+
240
194+
],
177195
"parameters": {
178196
"description": "Search the web for information. Input: a search query string.",
179197
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = { q: query }; }\nconst q = input.q || input.query || query;\nconst result = await helpers.httpRequest({\n method: 'GET',\n url: `http://searxng:8080/search?q=${encodeURIComponent(q)}&format=json&language=de`,\n headers: { 'Accept': 'application/json' }\n});\nconst hits = (result.results || []).slice(0, 5).map(r =>\n `${r.title}\\n${r.url}\\n${r.content || ''}`\n).join('\\n\\n');\nreturn hits || 'Keine Ergebnisse gefunden.';",
@@ -185,10 +203,13 @@
185203
"name": "MCP Client",
186204
"type": "@n8n/n8n-nodes-langchain.toolCode",
187205
"typeVersion": 1.2,
188-
"position": [736, 240],
206+
"position": [
207+
736,
208+
240
209+
],
189210
"parameters": {
190211
"description": "Calls a tool on an MCP server. Use when an installed MCP skill can help with the task. Required parameters: mcp_url (server URL), tool_name (tool to call), arguments (JSON object). Check available MCP servers in the system prompt.",
191-
"jsCode": "const qType = typeof query;\nlet mcpUrl = '', toolName = '', args = {};\n\nif (qType === 'object' && query !== null) {\n mcpUrl = query.mcp_url || '';\n toolName = query.tool_name || '';\n args = query.arguments || {};\n} else if (qType === 'string') {\n try {\n const parsed = JSON.parse(query);\n mcpUrl = parsed.mcp_url || '';\n toolName = parsed.tool_name || '';\n args = parsed.arguments || {};\n } catch(e) {\n return 'Parse error: query=' + String(query).substring(0,200) + ' type=' + qType;\n }\n} else {\n return 'Unexpected query type: ' + qType + ' value: ' + String(query).substring(0,200);\n}\n\nif (typeof args === 'string') try { args = JSON.parse(args); } catch(e) {}\nif (!mcpUrl || !toolName) return 'Fehler: mcp_url=' + mcpUrl + ' tool_name=' + toolName;\n\nfunction parseSSE(raw) {\n const str = String(raw);\n let lines = str.split('\\n');\n if (lines.length <= 1) lines = str.split('\\\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].startsWith('data: ')) {\n return JSON.parse(lines[i].substring(6));\n }\n }\n return null;\n}\n\ntry {\n let authHeader = null;\n try {\n const row = await helpers.httpRequest({method:'GET',url:'{{SUPABASE_URL}}/rest/v1/mcp_registry?mcp_url=eq.'+encodeURIComponent(mcpUrl)+'&select=auth_type,auth_token&limit=1',headers:{apikey:'{{SUPABASE_SERVICE_KEY}}',Authorization:'Bearer {{SUPABASE_SERVICE_KEY}}'},json:true});\n if (row && row[0] && row[0].auth_type && row[0].auth_type !== 'none' && row[0].auth_token) {\n authHeader = row[0].auth_type === 'bearer' ? 'Bearer '+row[0].auth_token : row[0].auth_token;\n }\n } catch(e) {}\n\n const hdrs = {'Content-Type':'application/json','Accept':'application/json, text/event-stream'};\n if (authHeader) hdrs['Authorization'] = authHeader;\n\n const init = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2024-11-05',capabilities:{},clientInfo:{name:'n8n-claw-bg-checker',version:'1.0'}}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n const sid = init.headers && init.headers['mcp-session-id'];\n if (sid) hdrs['mcp-session-id'] = sid;\n\n await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'}),\n json: false\n });\n\n const listResp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:2,method:'tools/list',params:{}}),\n json: false\n });\n const listResult = parseSSE(listResp);\n if (listResult && listResult.result && listResult.result.tools) {\n const tool = listResult.result.tools.find(t => t.name === toolName);\n if (tool && tool.inputSchema && tool.inputSchema.required) {\n for (const req of tool.inputSchema.required) {\n if (args[req] === undefined || args[req] === null) {\n args[req] = '';\n }\n }\n }\n }\n\n const resp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:3,method:'tools/call',params:{name:toolName,arguments:args}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n\n const result = parseSSE(resp.body || resp);\n if (!result) return 'MCP: Keine Daten. Response: ' + String(resp.body || resp).substring(0,500);\n if (result.error) return 'MCP Error: ' + JSON.stringify(result.error);\n return result.result?.content?.[0]?.text || JSON.stringify(result.result);\n} catch(e) {\n return 'MCP Error: ' + e.message;\n}",
212+
"jsCode": "const qType = typeof query;\nlet mcpUrl = '', toolName = '', args = {};\n\nif (qType === 'object' && query !== null) {\n mcpUrl = query.mcp_url || '';\n toolName = query.tool_name || '';\n args = query.arguments || {};\n} else if (qType === 'string') {\n try {\n const parsed = JSON.parse(query);\n mcpUrl = parsed.mcp_url || '';\n toolName = parsed.tool_name || '';\n args = parsed.arguments || {};\n } catch(e) {\n return 'Parse error: query=' + String(query).substring(0,200) + ' type=' + qType;\n }\n} else {\n return 'Unexpected query type: ' + qType + ' value: ' + String(query).substring(0,200);\n}\n\nif (typeof args === 'string') try { args = JSON.parse(args); } catch(e) {}\nif (!mcpUrl || !toolName) return 'Fehler: mcp_url=' + mcpUrl + ' tool_name=' + toolName;\n\nfunction parseSSE(raw) {\n const str = String(raw);\n let lines = str.split('\\n');\n if (lines.length <= 1) lines = str.split('\\\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].startsWith('data: ')) {\n return JSON.parse(lines[i].substring(6));\n }\n }\n return null;\n}\n\ntry {\n let authHeader = null;\n try {\n const row = await helpers.httpRequest({method:'GET',url:'{{SUPABASE_URL}}/rest/v1/mcp_registry?mcp_url=eq.'+encodeURIComponent(mcpUrl)+'&select=auth_type,auth_token&limit=1',headers:{apikey:'{{SUPABASE_SERVICE_KEY}}',Authorization:'Bearer {{SUPABASE_SERVICE_KEY}}'},json:true});\n if (row && row[0] && row[0].auth_type && row[0].auth_type !== 'none' && row[0].auth_token) {\n authHeader = row[0].auth_type === 'bearer' ? 'Bearer '+row[0].auth_token : row[0].auth_token;\n }\n } catch(e) {}\n\n const hdrs = {'Content-Type':'application/json','Accept':'application/json, text/event-stream'};\n if (authHeader) hdrs['Authorization'] = authHeader;\n\n const init = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2024-11-05',capabilities:{},clientInfo:{name:'n8n-claw-bg-checker',version:'1.0'}}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n const sid = init.headers && init.headers['mcp-session-id'];\n if (sid) hdrs['mcp-session-id'] = sid;\n\n await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'}),\n json: false\n });\n\n const listResp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:2,method:'tools/list',params:{}}),\n json: false\n });\n const listResult = parseSSE(listResp);\n let toolSchema = null;\n if (listResult && listResult.result && listResult.result.tools) {\n const tool = listResult.result.tools.find(t => t.name === toolName);\n if (tool && tool.inputSchema) toolSchema = tool.inputSchema;\n }\n\n function schemaHint(problem) {\n if (!toolSchema) return null;\n return 'Tool \"' + toolName + '\" call rejected: ' + problem +\n '\\n\\nCorrect input schema:\\n' + JSON.stringify(toolSchema, null, 2) +\n '\\n\\nRetry the tool call with parameter names matching this schema exactly.';\n }\n\n if (toolSchema && toolSchema.properties) {\n const allowed = Object.keys(toolSchema.properties);\n const required = toolSchema.required || [];\n const unknown = Object.keys(args).filter(k => !allowed.includes(k));\n const missing = required.filter(r => args[r] === undefined || args[r] === null || args[r] === '');\n if (unknown.length > 0 || missing.length > 0) {\n const problems = [];\n if (unknown.length > 0) problems.push('unknown args [' + unknown.join(', ') + ']');\n if (missing.length > 0) problems.push('missing required args [' + missing.join(', ') + ']');\n return schemaHint(problems.join('; '));\n }\n }\n\n const resp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:3,method:'tools/call',params:{name:toolName,arguments:args}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n\n const result = parseSSE(resp.body || resp);\n if (!result) return 'MCP: Keine Daten. Response: ' + String(resp.body || resp).substring(0,500);\n if (result.error) {\n return schemaHint('server error: ' + JSON.stringify(result.error)) || ('MCP Error: ' + JSON.stringify(result.error));\n }\n if (result.result && result.result.isError) {\n const errText = (result.result.content && result.result.content[0] && result.result.content[0].text) || JSON.stringify(result.result);\n return schemaHint('tool returned error: ' + errText) || ('MCP Tool Error: ' + errText);\n }\n return result.result?.content?.[0]?.text || JSON.stringify(result.result);\n} catch(e) {\n return 'MCP Error: ' + e.message;\n}",
192213
"specifyInputSchema": false
193214
}
194215
},
@@ -197,7 +218,10 @@
197218
"name": "Web Reader",
198219
"type": "@n8n/n8n-nodes-langchain.toolCode",
199220
"typeVersion": 1.2,
200-
"position": [864, 240],
221+
"position": [
222+
864,
223+
240
224+
],
201225
"parameters": {
202226
"description": "Read a webpage and return its content as clean markdown. Input: URL string or JSON with url field. Use this instead of HTTP Tool when you need to read webpage content (articles, docs, blog posts). Returns clean text without HTML boilerplate.",
203227
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = { url: query }; }\nif (!input.url) return 'Error: url is required';\ntry {\n const resp = await helpers.httpRequest({\n method: 'POST',\n url: 'http://crawl4ai:11235/crawl',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ urls: [input.url], crawler_config: { type: 'CrawlerRunConfig', params: { stream: false, cache_mode: 'bypass' } } }),\n timeout: 30000\n });\n const results = resp.results || resp;\n const result = Array.isArray(results) ? results[0] : results;\n if (!result.success) return 'Error: ' + (result.error_message || 'Crawl failed');\n const mdObj = result.markdown || {};\n const md = typeof mdObj === 'string' ? mdObj : (mdObj.raw_markdown || '');\n return md.substring(0, 8000);\n} catch(e) {\n return 'Error fetching page: ' + e.message;\n}",
@@ -209,7 +233,10 @@
209233
"name": "Parse Result",
210234
"type": "n8n-nodes-base.code",
211235
"typeVersion": 2,
212-
"position": [740, 0],
236+
"position": [
237+
740,
238+
0
239+
],
213240
"parameters": {
214241
"jsCode": "const agentOutput = $('Checker Agent').first().json.output || '';\nconst isSilent = agentOutput.includes('[SILENT]');\n\nreturn [{\n json: {\n notify: !isSilent,\n message: isSilent ? '' : agentOutput\n }\n}];"
215242
}

0 commit comments

Comments
 (0)