Skip to content

feat: structured MCP-server config endpoint (Layer 2 follow-up to #590) #598

Description

@pavshulin

Summary

Restore the user-facing ability to add/edit MCP servers post-deploy, lost in #590 (AISEC-C2 RCE closure), via a structured endpoint that validates inputs at the schema level — not raw JSON.

Context

#590 (Apr 30 2026) closed the .mcp.json RCE-by-config bypass by removing .mcp.json and .mcp.json.template from the user-facing ALLOWED_CREDENTIAL_PATHS and adding them to the agent-server's EDIT_PROTECTED_PATHS. The capability was insecure-by-design: any owner with raw write access to .mcp.json could inject {"mcpServers": {"e": {"command": "/bin/sh", "args": ["-c", "cat /proc/1/environ"]}}} and gain RCE on agent restart.

UI feature regression after #590:

  • "Save File" in CredentialsPanel.vue for .mcp.json returns 400 (disallowed path)
  • Same for .mcp.json.template
  • Owners can still add MCP servers at agent creation via the GitHub template repo's template.yaml
  • Quick Inject .env (the dominant credential workflow) is unchanged

Proposed design

New endpoint: POST /api/agents/{name}/mcp-servers (and PUT/DELETE for individual entries by id).

Request body shape:

{
  "id": "google-workspace",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-google-workspace"],
  "env": { "GOOGLE_TOKEN": "${GOOGLE_TOKEN}" }
}

Server-side validation:

  • transport{stdio, http, sse} enum
  • For stdio: command MUST be in a managed allowlist (npx, uvx, docker, python, python3, node, bun, deno). No absolute paths, no bash -c, no sh -c.
  • args rejected if any element contains shell metacharacters that could escape the runner (;, |, $(, backticks, redirects)
  • For http/sse: HTTPS-only; hostname allowlist (npm registry, github raw, GCP/AWS endpoints, etc.) — same SSRF rules as SEC: Server-Side Request Forgery leads to Denial of Service #179 (skills library validation)
  • env values are template references resolved from .env (${VAR} → looked up in agent's .env file). Arbitrary string values rejected.

Owner-only authorization via get_owned_agent_by_name (consistent with inject_credentials).

Audit-logged via platform_audit_service (event_action="mcp_server_set").

UI replacement

Replace the raw JSON editor in CredentialsPanel.vue (.mcp.json row) with:

  • "Add MCP Server" button → modal with structured form (transport selector → conditional fields)
  • List of configured servers with edit/delete actions per row
  • "Test connection" button per server (optional, follow-up)

Acceptance Criteria

  • POST/PUT/DELETE /api/agents/{name}/mcp-servers endpoints with the structured schema above
  • Validation rejects: arbitrary command, shell metacharacters in args, non-HTTPS URLs, hostname outside allowlist, env values that aren't ${VAR} references
  • Server writes to agent's .mcp.json via the platform-internal /api/credentials/update flow (the only endpoint allowed to touch .mcp.json)
  • Audit-logged with full request body (post-validation, no secrets)
  • UI form replaces the .mcp.json raw editor in CredentialsPanel.vue
  • Unit tests: command allowlist, args sanitization, env template enforcement, hostname allowlist
  • Integration test: end-to-end add → restart agent → verify MCP server registered

Out of scope

References

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions