ai.fodda/mcp-server
version 1.46.85
- scanned 2026-10-16
- source github.com/piers-fawkes/fodda-mcp
- commit f515aebd41bf
- transport streamable-http
- files read 145
- methodology 0.2.0
- ruleset 0.1.1
No tag matching version 1.46.85 was found in this repository, so the scan read the default branch instead. The findings below describe that code, not necessarily the released version.
67measured on 98% of this axis
7 findings in test code are listed but not deducted; 1 file could not be parsed and was not scanned
94measured on 80% of this axis
0
94measured on 90% of this axis
79
The evidence
Every deduction below names the artefact that produced it. Nothing on this page was executed: the source was read, never run, and no running server was contacted.
Code Safety
Showing the 50 most severe of 52 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- src/toolHandlers.ts:300mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
// webpage_url will become available once the API surfaces it from Airtable if (g && g.webpage_url) { urls[gid] = g.webpage_url; } } - src/toolHandlers.ts:346mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
val = stripRoutingInstruction(val); } out[key] = val; } } - src/toolHandlers.ts:3288mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const bLower = (b || '').toLowerCase(); if (bLower && bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(t.graphId); } - src/toolHandlers.ts:3381mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const bLower = b.toLowerCase(); if (bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(trend.graphId); - src/toolHandlers.ts:3382mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(trend.graphId); } - src/toolHandlers.ts:3507mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { if (graphId.includes('earnings') || graphId.includes('finance')) continue; competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(graphId); - src/toolHandlers.ts:3508mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (graphId.includes('earnings') || graphId.includes('finance')) continue; competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(graphId); } - src/toolHandlers.ts:3522mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const bLower = b.toLowerCase(); if (bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(graphId); - src/toolHandlers.ts:3523mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (bLower !== brandLower && !bLower.includes(brandLower) && !brandLower.includes(bLower)) { competitorCounts[b] = (competitorCounts[b] || 0) + 1; if (!competitorGraphs[b]) competitorGraphs[b] = new Set(); competitorGraphs[b].add(graphId); } - packages/agent-swarm-test/src/swarm.ts:113mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
if (options.mode === 'spt') { // Step 1: Unauthenticated request to trigger & verify MPP HTTP 402 Challenge const challengeRes = await fetch(targetUrl.toString(), { method: 'GET', headers: { - packages/agent-swarm-test/src/swarm.ts:124mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
sptChallengeVerified = true; // Step 2: Attach SPT bearer token and retry const retryRes = await fetch(targetUrl.toString(), { method: 'GET', headers: { - packages/agent-swarm-test/src/swarm.ts:147mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// apikey mode: Attach X-API-Key header directly const apiKey = process.env.FODDA_API_KEY || 'sk_test_simulated_key'; const res = await fetch(targetUrl.toString(), { method: 'GET', headers: { - packages/fodda-agentkit/src/foddaActionProvider.ts:74mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
} let response = await fetch(url, { method: "POST", headers, - packages/fodda-agentkit/src/foddaActionProvider.ts:147mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
// 4. Retry query attaching the settled payment transaction response = await fetch(url, { method: "POST", headers: { - scratch/probe_api.mjs:29mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
async function call(method, path, body) { const res = await fetch(BASE + path, { method, headers: headers(method, path, body), body: body ? JSON.stringify(body) : undefined, - scratch/probe_ce.mjs:3mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
import crypto from 'node:crypto'; const BASE=process.env.FODDA_API_URL||'https://api.fodda.ai',KEY=process.env.FODDA_API_KEY,SECRET=process.env.FODDA_MCP_SECRET,USER='piers.fawkes@psfk.com'; function call(method,path,body){const ts=Date.now().toString();const h={'X-API-Key':KEY,'X-User-Id':USER,'X-Fodda-Timestamp':ts,'X-Fodda-Billing':'mcp-orchestrated','Content-Type':'application/json'};if(SECRET){const p=(method==='POST'&&body)?ts+'.'+JSON.stringify(body):ts+'.'+path;h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(p).digest('hex');}return fetch(BASE+path,{method,headers:h,body:body?JSON.stringify(body):undefined}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)}));} // find CE / consumer-electronics / expert graphs const g=await call('GET','/v1/graphs'); - scratch/probe_ce2.mjs:3mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
import crypto from 'node:crypto'; const BASE=process.env.FODDA_API_URL||'https://api.fodda.ai',KEY=process.env.FODDA_API_KEY,SECRET=process.env.FODDA_MCP_SECRET,USER='piers.fawkes@psfk.com'; function call(method,path,body){const ts=Date.now().toString();const h={'X-API-Key':KEY,'X-User-Id':USER,'X-Fodda-Timestamp':ts,'X-Fodda-Billing':'mcp-orchestrated','Content-Type':'application/json'};if(SECRET){const p=(method==='POST'&&body)?ts+'.'+JSON.stringify(body):ts+'.'+path;h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(p).digest('hex');}return fetch(BASE+path,{method,headers:h,body:body?JSON.stringify(body):undefined}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)}));} for(const gid of ['ce-design','psfk-consumer-electronics']){ let rows=[]; - scratch/probe_evidence.mjs:10mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const h = { 'X-API-Key': KEY, 'X-User-Id': USER, 'X-Fodda-Timestamp': ts, 'X-Fodda-Billing': 'mcp-orchestrated', 'Content-Type': 'application/json' }; if (SECRET) { const p = (method==='POST'&&body)? ts+'.'+JSON.stringify(body): ts+'.'+path; h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(p).digest('hex'); } return fetch(BASE+path,{method,headers:h,body:body?JSON.stringify(body):undefined}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)})); } const r = await call('POST', '/v1/graphs/retail/search', { query: 'small format curated retail concepts', limit: 6, include_evidence: true }); - scratch/probe_evidence2.mjs:4mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
import crypto from 'node:crypto'; const BASE=process.env.FODDA_API_URL||'https://api.fodda.ai',KEY=process.env.FODDA_API_KEY,SECRET=process.env.FODDA_MCP_SECRET,USER='piers.fawkes@psfk.com'; function call(method,path,body){const ts=Date.now().toString();const h={'X-API-Key':KEY,'X-User-Id':USER,'X-Fodda-Timestamp':ts,'X-Fodda-Billing':'mcp-orchestrated','Content-Type':'application/json'};if(SECRET){const p=(method==='POST'&&body)?ts+'.'+JSON.stringify(body):ts+'.'+path;h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(p).digest('hex');}return fetch(BASE+path,{method,headers:h,body:body?JSON.stringify(body):undefined}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)}));} const r=await call('POST','/v1/graphs/beauty/search',{query:'small format curated retail concepts',limit:6,include_evidence:true}); const rows=r.json?.rows||r.json?.results||[]; - scratch/probe_node.mjs:3mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
import crypto from 'node:crypto'; const BASE=process.env.FODDA_API_URL||'https://api.fodda.ai',KEY=process.env.FODDA_API_KEY,SECRET=process.env.FODDA_MCP_SECRET,USER='piers.fawkes@psfk.com'; function call(path){const ts=Date.now().toString();const h={'X-API-Key':KEY,'X-User-Id':USER,'X-Fodda-Timestamp':ts,'X-Fodda-Billing':'mcp-orchestrated','Content-Type':'application/json'};if(SECRET)h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(ts+'.'+path).digest('hex');return fetch(BASE+path,{headers:h}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)}));} for(const g of ['beauty','retail','food','tech']){ const r=await call(`/v1/graphs/${g}/nodes/6779`); - scratch/probe_node2.mjs:3mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
import crypto from 'node:crypto'; const BASE=process.env.FODDA_API_URL||'https://api.fodda.ai',KEY=process.env.FODDA_API_KEY,SECRET=process.env.FODDA_MCP_SECRET,USER='piers.fawkes@psfk.com'; function call(path){const ts=Date.now().toString();const h={'X-API-Key':KEY,'X-User-Id':USER,'X-Fodda-Timestamp':ts,'X-Fodda-Billing':'mcp-orchestrated','Content-Type':'application/json'};if(SECRET)h['X-Fodda-Signature']=crypto.createHmac('sha256',SECRET).update(ts+'.'+path).digest('hex');return fetch(BASE+path,{headers:h}).then(async r=>({status:r.status,json:await r.json().catch(()=>null)}));} // 6678 = "Small-Format Store Growth", a CLEAN retail node (slug "retail"); 6779 = corrupted mega-slug cluster node for(const nid of ['6678','6779']){ - scratch/probe_pii.mjs:11mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const headers = { 'X-API-Key': KEY, 'X-User-Id': CALLER, 'X-Fodda-Timestamp': ts, 'X-Fodda-Billing': 'mcp-orchestrated', 'Content-Type': 'application/json' }; if (SECRET) headers['X-Fodda-Signature'] = crypto.createHmac('sha256', SECRET).update(ts + '.' + path).digest('hex'); const res = await fetch(BASE + path, { headers }); const d = await res.json(); const graphs = d.graphs || []; - scratch/probe_search.mjs:10mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const h = { 'X-API-Key': KEY, 'X-User-Id': USER, 'X-Fodda-Timestamp': ts, 'X-Fodda-Billing': 'mcp-orchestrated', 'Content-Type': 'application/json' }; if (SECRET) { const p = (method==='POST'&&body)? ts+'.'+JSON.stringify(body) : ts+'.'+path; h['X-Fodda-Signature'] = crypto.createHmac('sha256', SECRET).update(p).digest('hex'); } return fetch(BASE+path, { method, headers: h, body: body?JSON.stringify(body):undefined }).then(async r => ({ status: r.status, json: await r.json().catch(()=>null) })); } for (const g of ['retail','food','tech']) { - scratch/run_mcp_review.py:94mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
) with urllib.request.urlopen(req, timeout=120, context=ctx) as resp: result = json.loads(resp.read()) - scripts/profile_graphs.ts:24mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const headers = { 'X-API-Key': FODDA_API_KEY, 'Content-Type': 'application/json' }; try { const resp = method === 'POST' ? await axios.post(url, body, { headers }) : await axios.get(url, { headers }); return resp.data; } catch (err: any) { - scripts/profile_graphs.ts:24mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const headers = { 'X-API-Key': FODDA_API_KEY, 'Content-Type': 'application/json' }; try { const resp = method === 'POST' ? await axios.post(url, body, { headers }) : await axios.get(url, { headers }); return resp.data; } catch (err: any) { - scripts/sync-descriptions-from-airtable.mjs:38mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
do { const url = `https://api.airtable.com/v0/${BASE_ID}/${TABLE_ID}${offset ? `?offset=${offset}` : ''}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${AIRTABLE_API_KEY}` } }); - scripts/sync-discovery.mjs:45mcp-js-path-join-unvalidated (medium/low)
A filesystem path is assembled from a non-literal and read or written without confinement. path.join does not resolve away `..`, and path.resolve discards the base entirely on an absolute second component.
// ───────────────────────────────────────────────────────────────────────────── const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8'); const write = (rel, data) => fs.writeFileSync(path.join(ROOT, rel), data); const readJSON = (rel) => JSON.parse(read(rel)); - scripts/sync-discovery.mjs:46mcp-js-path-join-unvalidated (medium/low)
A filesystem path is assembled from a non-literal and read or written without confinement. path.join does not resolve away `..`, and path.resolve discards the base entirely on an absolute second component.
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8'); const write = (rel, data) => fs.writeFileSync(path.join(ROOT, rel), data); const readJSON = (rel) => JSON.parse(read(rel)); - scripts/sync-discovery.mjs:72mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
async function fetchJSON(url) { try { const res = await fetch(url, { signal: AbortSignal.timeout(8000) }); if (!res.ok) return { _error: `HTTP ${res.status}` }; return await res.json(); - src/catalogCache.ts:238mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const url = `${API_BASE_URL}/v1/graphs/catalog`; try { const response = await axios.get(url, { timeout: 10000 }); return response.data as CatalogResponse; } catch (err: any) { - src/catalogCache.ts:255mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
try { const response = await axios.get(url, { headers, timeout: 10000 }); const raw = Array.isArray(response.data) ? response.data : (response.data.analysts || []); return raw.map(normalizeAnalyst); - src/errorHandling.ts:28mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
async function fetchAgentCheckoutLink(email: string | null, source: string = 'mcp'): Promise<string | null> { try { const response = await axios.post( `${APP_BASE_URL}/api/account/checkout/agent-session`, { email, source }, - src/errorHandling.ts:50mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
? { email: emailOrAccountId } : { accountId: emailOrAccountId }; const response = await axios.post( `${APP_BASE_URL}/api/account/setup-url`, body, - src/foddaClient.ts:175mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const startTime = Date.now(); const response = method === 'GET' ? await axios.get(url, { headers, timeout: AXIOS_TIMEOUT_MS }) : method === 'PATCH' ? await axios.patch(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS }) - src/foddaClient.ts:177mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
? await axios.get(url, { headers, timeout: AXIOS_TIMEOUT_MS }) : method === 'PATCH' ? await axios.patch(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS }) : await axios.post(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS }); const latencyMs = Date.now() - startTime; - src/foddaClient.ts:178mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
: method === 'PATCH' ? await axios.patch(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS }) : await axios.post(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS }); const latencyMs = Date.now() - startTime; console.error(`[foddaRequest] ${method} ${path} completed in ${latencyMs}ms`); - src/index.ts:681mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
async function validateSpt(spt: string): Promise<{ valid: boolean; max_amount_cents: number | null; prices: Record<string, number>; error?: string }> { try { const resp = await axios.get(`${API_BASE_URL}/v1/spt/validate`, { headers: { 'Authorization': `Bearer ${spt}` }, timeout: 15000, - src/index.ts:1093mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
try { const response = await axios.get(`${websiteBaseUrl}/api/mcp-tokens/${token}`, { headers, timeout: 5000, - src/index.ts:1160mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
try { const resolveUrl = `${API_BASE_URL}/v1/auth/clerk-resolve`; const resolveResp = await axios.post(resolveUrl, { clerk_jwt: rawBearer }, { headers: { 'X-Internal-Key': process.env.FODDA_INTERNAL_API_KEY || '' }, timeout: 5000, - src/pricingCache.ts:443mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${PRICING_TABLE_ID}`; const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${AIRTABLE_API_KEY}` }, params: { 'filterByFormula': '{isActive}' }, - src/services/notifications.ts:209mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
if (slackWebhookUrl) { const resp = await fetch(slackWebhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - src/toolHandlers.ts:5630mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
}; const response = await axios.post( `${APP_BASE_URL}/api/account/trial-convert`, convertBody, - src/toolHandlers.ts:6403mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
for (const url of webhookUrls) { try { await axios.post(url, payload, { headers: { 'Content-Type': 'application/json', - src/verify_tools_endpoint.ts:20mcp-js-ssrf-nonliteral-url (medium/low)
An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
while (Date.now() - startTime < 30000) { try { response = await axios.get(`http://127.0.0.1:${testPort}/health`, { timeout: 1500 }); if (response && response.status === 200) break; } catch { - src/test_book_a_call_live.ts:29mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
}; const res = await axios({ method, url, - src/test_earnings_activity_scope.ts:88mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
try { const res = await fetch(baseUrl + pathStr, { headers }); assert(res.status === 200, `Live API returned HTTP ${res.status} for ?sector=apparel&activity=sustainability`); const data: any = await res.json(); - src/test_earnings_hygiene_and_binding.ts:99mcp-js-path-join-unvalidated (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. A filesystem path is assembled from a non-literal and read or written without confinement. path.join does not resolve away `..`, and path.resolve discards the base entirely on an absolute second component.
console.log('\n--- 4. Testing tools-manifest.json Integrity ---'); const manifestRaw = fs.readFileSync(path.resolve(__dirname, '../tools-manifest.json'), 'utf8'); const manifest = JSON.parse(manifestRaw); - src/test_intelligence_and_verifier.ts:56mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
}; const res = await axios({ method, url: `${baseUrl}${endpoint}`, - src/test_live_deployed_mcp_search.ts:55mcp-js-ssrf-nonliteral-url (medium/low)
Test code — listed, not deducted: `03` §3 grades the code a user runs. An outbound request is made to a URL that is not a literal. If any part is caller-supplied the server can be aimed at internal hosts it can reach and the caller cannot.
const filterFormula = encodeURIComponent(`{question} = '${testQuery}'`); const searchResp = await axios.get(`${airtableUrl}?filterByFormula=${filterFormula}`, { headers }); const records = searchResp.data.records || [];
Auth Posture
- authentication_model
auth enforcement present at a call site in the source (inbound token verification) and documented in the README; "refused without credentials" (`03` §4) established by reading the code rather than by connecting
- transport_security
declared endpoint(s) are HTTPS: https://mcp.fodda.ai/mcp; scored 80, not 100: `03` §4 reserves 100 for HTTPS *with HSTS*, and an HSTS header can only be seen by connecting, which `04` §9 does not do. This is an unverifiable band, not an observed absence.
- secret_handling
detect-secrets matched 30 credential-shaped string(s), every one from a keyword or entropy heuristic (Hex High Entropy String, Secret Keyword). Those flag placeholders, variable names and content hashes as readily as credentials, so they do not establish the committed credential `03` §4's 0 asserts, and this sub-check is not scored; Secret Keyword at PUBLICATION_SUMMARY.md:51; Secret Keyword at briefs/Brief - Clerk OAuth Identity Resolution and Skills API Metadata (API Agent).md:32; Hex High Entropy String at briefs/Brief MCP Response Improvements.md:42; Secret Keyword at scratch/probe_ce.mjs:2; Secret Keyword at scratch/probe_ce2.mjs:2; Secret Keyword at scratch/probe_evidence2.mjs:3; Secret Keyword at scratch/probe_node.mjs:2; Secret Keyword at scratch/probe_node2.mjs:2; Secret Keyword at scripts/test_chatgpt_live.mjs:28; Secret Keyword at scripts/test_chatgpt_live.mjs:159; …and 20 more matches
- authorization_granularity
51 tool registrations with conditional access logic present in the same source
Dependency Health
Showing the 50 most severe of 239 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- package-lock.jsonproxy-addr 2.0.7 — GHSA-jqcg-44mw-7w3h
transitive, CVSS 9.1, fixed in 2.0.8
https://osv.dev/vulnerability/GHSA-jqcg-44mw-7w3h - test-mcp-server/package-lock.jsonproxy-addr 2.0.7 — GHSA-jqcg-44mw-7w3h
transitive, CVSS 9.1, fixed in 2.0.8
https://osv.dev/vulnerability/GHSA-jqcg-44mw-7w3h - package-lock.jsonaxios 1.13.5 — GHSA-35jp-ww65-95wh
direct, CVSS 8.7, fixed in 1.16.0
https://osv.dev/vulnerability/GHSA-35jp-ww65-95wh - package-lock.jsonaxios 1.13.5 — GHSA-542g-h47m-68v8
direct, CVSS 8.2, fixed in 1.20.0
https://osv.dev/vulnerability/GHSA-542g-h47m-68v8 - package-lock.jsonaxios 1.13.5 — GHSA-p92q-9vqr-4j8v
direct, CVSS 8.2, fixed in 1.16.0
https://osv.dev/vulnerability/GHSA-p92q-9vqr-4j8v - package-lock.json@modelcontextprotocol/sdk 1.27.1 — GHSA-6qxp-vccf-f47h
direct, CVSS 7.5, fixed in 1.31.0
https://osv.dev/vulnerability/GHSA-6qxp-vccf-f47h - test-mcp-server/package-lock.json@modelcontextprotocol/sdk 1.27.1 — GHSA-6qxp-vccf-f47h
direct, CVSS 7.5, fixed in 1.31.0
https://osv.dev/vulnerability/GHSA-6qxp-vccf-f47h - package-lock.jsonaxios 1.13.5 — GHSA-62hf-57xw-28j9
direct, CVSS 7.5, fixed in 1.15.1
https://osv.dev/vulnerability/GHSA-62hf-57xw-28j9 - package-lock.jsonaxios 1.13.5 — GHSA-777c-7fjr-54vf
direct, CVSS 7.5, fixed in 1.16.0
https://osv.dev/vulnerability/GHSA-777c-7fjr-54vf - package-lock.jsonaxios 1.13.5 — GHSA-hfxv-24rg-xrqf
direct, CVSS 7.5, fixed in 1.16.0
https://osv.dev/vulnerability/GHSA-hfxv-24rg-xrqf - package-lock.jsonaxios 1.13.5 — GHSA-j5f8-grm9-p9fc
direct, CVSS 7.5, fixed in 1.16.0
https://osv.dev/vulnerability/GHSA-j5f8-grm9-p9fc - package-lock.jsonaxios 1.13.5 — GHSA-6chq-wfr3-2hj9
direct, CVSS 7.4, fixed in 1.15.1
https://osv.dev/vulnerability/GHSA-6chq-wfr3-2hj9 - package-lock.jsonaxios 1.13.5 — GHSA-pf86-5x62-jrwf
direct, CVSS 7.4, fixed in 1.15.1
https://osv.dev/vulnerability/GHSA-pf86-5x62-jrwf - package-lock.jsonaxios 1.13.5 — GHSA-q8qp-cvcw-x6jj
direct, CVSS 7.4, fixed in 1.15.2
https://osv.dev/vulnerability/GHSA-q8qp-cvcw-x6jj - package-lock.jsonaxios 1.13.5 — GHSA-pmwg-cvhr-8vh7
direct, CVSS 7.2, fixed in 1.15.1
https://osv.dev/vulnerability/GHSA-pmwg-cvhr-8vh7 - package-lock.jsonaxios 1.13.5 — GHSA-3g43-6gmg-66jw
direct, CVSS 7.0, fixed in 1.15.2
https://osv.dev/vulnerability/GHSA-3g43-6gmg-66jw - package-lock.jsonaxios 1.13.5 — GHSA-3pq3-5fj3-cg6v
direct, CVSS 7.0, fixed in 1.20.0
https://osv.dev/vulnerability/GHSA-3pq3-5fj3-cg6v - fodda-vscode/package-lock.jsonbraces 3.0.3 — GHSA-vfj7-8cjw-p6xm
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-vfj7-8cjw-p6xm - fodda-vscode/package-lock.jsonform-data 4.0.5 — GHSA-hmw2-7cc7-3qxx
transitive, CVSS 8.7, fixed in 4.0.6
https://osv.dev/vulnerability/GHSA-hmw2-7cc7-3qxx - package-lock.jsonform-data 4.0.5 — GHSA-hmw2-7cc7-3qxx
transitive, CVSS 8.7, fixed in 4.0.6
https://osv.dev/vulnerability/GHSA-hmw2-7cc7-3qxx - fodda-vscode/package-lock.jsonlinkify-it 5.0.0 — GHSA-22p9-wv53-3rq4
transitive, CVSS 8.7, fixed in 5.0.1
https://osv.dev/vulnerability/GHSA-22p9-wv53-3rq4 - package-lock.jsonnode-forge 1.4.0 — GHSA-86w9-cpqp-85rv
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-86w9-cpqp-85rv - package-lock.jsonprotobufjs 7.5.5 — GHSA-75px-5xx7-5xc7
transitive, CVSS 8.1, fixed in 7.5.6
https://osv.dev/vulnerability/GHSA-75px-5xx7-5xc7 - fodda-vscode/package-lock.jsonbrace-expansion 1.1.14 — GHSA-3jxr-9vmj-r5cp
transitive, CVSS 7.7, fixed in 1.1.16
https://osv.dev/vulnerability/GHSA-3jxr-9vmj-r5cp - fodda-vscode/package-lock.jsonbrace-expansion 5.0.5 — GHSA-3jxr-9vmj-r5cp
transitive, CVSS 7.7, fixed in 5.0.7
https://osv.dev/vulnerability/GHSA-3jxr-9vmj-r5cp - package-lock.jsonbrace-expansion 2.1.0 — GHSA-3jxr-9vmj-r5cp
transitive, CVSS 7.7, fixed in 2.1.2
https://osv.dev/vulnerability/GHSA-3jxr-9vmj-r5cp - package-lock.jsonip-address 10.0.1 — GHSA-mwp4-54f8-5fhr
transitive, CVSS 7.7, fixed in 10.3.1
https://osv.dev/vulnerability/GHSA-mwp4-54f8-5fhr - test-mcp-server/package-lock.jsonip-address 10.1.0 — GHSA-mwp4-54f8-5fhr
transitive, CVSS 7.7, fixed in 10.3.1
https://osv.dev/vulnerability/GHSA-mwp4-54f8-5fhr - package-lock.jsonprotobufjs 7.5.5 — GHSA-66ff-xgx4-vchm
transitive, CVSS 7.7, fixed in 7.5.6
https://osv.dev/vulnerability/GHSA-66ff-xgx4-vchm - fodda-vscode/package-lock.jsontmp 0.2.5 — GHSA-ph9p-34f9-6g65
transitive, CVSS 7.7, fixed in 0.2.6
https://osv.dev/vulnerability/GHSA-ph9p-34f9-6g65 - package-lock.json@grpc/grpc-js 1.14.3 — GHSA-5375-pq7m-f5r2
transitive, CVSS 7.5, fixed in 1.14.4
https://osv.dev/vulnerability/GHSA-5375-pq7m-f5r2 - package-lock.json@grpc/grpc-js 1.14.3 — GHSA-99f4-grh7-6pcq
transitive, CVSS 7.5, fixed in 1.14.4
https://osv.dev/vulnerability/GHSA-99f4-grh7-6pcq - package-lock.json@hono/node-server 1.19.9 — GHSA-wc8c-qw6v-h7f6
transitive, CVSS 7.5, fixed in 1.19.10
https://osv.dev/vulnerability/GHSA-wc8c-qw6v-h7f6 - fodda-vscode/package-lock.jsonbrace-expansion 1.1.14 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 1.1.19
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - fodda-vscode/package-lock.jsonbrace-expansion 5.0.5 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 5.0.10
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - package-lock.jsonbrace-expansion 2.1.0 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 2.1.5
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - fodda-vscode/package-lock.jsonbrace-expansion 1.1.14 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 1.1.17
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - fodda-vscode/package-lock.jsonbrace-expansion 5.0.5 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 5.0.8
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - package-lock.jsonbrace-expansion 2.1.0 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 2.1.3
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - fodda-vscode/package-lock.jsonbrace-expansion 1.1.14 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 1.1.20
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - fodda-vscode/package-lock.jsonbrace-expansion 5.0.5 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 5.0.11
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - package-lock.jsonbrace-expansion 2.1.0 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 2.1.6
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - fodda-vscode/package-lock.jsonbrace-expansion 1.1.14 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 1.1.18
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - fodda-vscode/package-lock.jsonbrace-expansion 5.0.5 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 5.0.9
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - package-lock.jsonbrace-expansion 2.1.0 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 2.1.4
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - package-lock.jsonexpress-rate-limit 8.2.1 — GHSA-46wh-pxpv-q5gq
transitive, CVSS 7.5, fixed in 8.2.2
https://osv.dev/vulnerability/GHSA-46wh-pxpv-q5gq - fodda-vscode/package-lock.jsonfast-uri 3.1.0 — GHSA-4c8g-83qw-93j6
transitive, CVSS 7.5, fixed in 3.1.3
https://osv.dev/vulnerability/GHSA-4c8g-83qw-93j6 - package-lock.jsonfast-uri 3.1.0 — GHSA-4c8g-83qw-93j6
transitive, CVSS 7.5, fixed in 3.1.3
https://osv.dev/vulnerability/GHSA-4c8g-83qw-93j6 - test-mcp-server/package-lock.jsonfast-uri 3.1.0 — GHSA-4c8g-83qw-93j6
transitive, CVSS 7.5, fixed in 3.1.3
https://osv.dev/vulnerability/GHSA-4c8g-83qw-93j6 - fodda-vscode/package-lock.jsonfast-uri 3.1.0 — GHSA-7p8r-x3mc-p8w7
transitive, CVSS 7.5, fixed in 3.1.5
https://osv.dev/vulnerability/GHSA-7p8r-x3mc-p8w7
Maintenance
- recency
last commit 4 day(s) before 2026-10-16
- issue_responsiveness
lifetime median (2 issue(s) in the last 6 months, under `03` §5's floor of 3): 3.7 day(s) to first maintainer comment
- release_cadence
released 9 day(s) ago (by releases), inside `03` §5's 30-day window
- bus_factor
3 authors with >= 5 commits in 12 months
- repository_signals
`03` §5 scores this by quartile WITHIN the server's category, which needs a distribution across every scanned server and so cannot be computed while scanning one. It is a post-pass over the crawl and that pass does not exist yet.
Transparency
- license
LICENSE present with no SPDX-License-Identifier tag; text not matched against any license this build can identify (`04` §7's classifier is not wired; see module docstring)
- readme_quality
README.md: installation instructions (+25), configuration documented (+25), exposed tools or resources enumerated (+30), states what the server does in the first 200 words (+20)
- declared_scopes
capabilities are described only in vague terms — `03` §7's 30 band, which needs no inference to establish
- changelog
CHANGELOG.md present
- security_contact
SECURITY_PACK.md present
Contest a finding
Every number here comes from a rule reading published source, and rules get things wrong. If something on this page is incorrect — or correct but mitigated by a check the rule cannot see — open an issue and we answer it in writing. A finding we got wrong is removed and the rule is fixed; one that stands is answered on the record.
Anyone can file: the maintainer, a researcher who disagrees with the classification, or a user who tested the server and could not reproduce it. Name this server and the rule or advisory ID shown beside the finding above. We aim to reply within five business days and to resolve within ten.