feat: add MCP API key functionality for ACP agents - #368
Conversation
- Introduced MCP API key generation for ACP agents, allowing for unique identification of agent actions. - Updated agent entity to include MCP API key hash and related fields. - Implemented API endpoints for generating MCP API keys for both project-specific and global agents. - Enhanced authentication middleware to support MCP API key validation, ensuring requests can be attributed to the correct agent. - Added necessary database migrations to support the new MCP API key structure. - Updated relevant tests to cover new functionality and ensure proper integration.
There was a problem hiding this comment.
Thanks for the PR — per-agent MCP API keys are a solid improvement over the shared AGENT_API_KEY. I read the full diff, ran the Go unit tests for the touched packages (all passed), and attempted the web typecheck, but the ephemeral runner has no node_modules installed so I could not validate the TypeScript build.
Overall the shape looks correct: SHA-256-only storage, key regeneration that immediately invalidates the old key, a partial unique index, and authn middleware that resolves the key directly to an agent. I do have a few requested changes and suggestions before merging.
Requested changes
-
Add unit tests for the new MCP key service methods.
GenerateAgentMCPKeyandGenerateGlobalAgentMCPKeycurrently have no direct test coverage that I could find, and the authentication fallback path inapikeysvc.AuthenticateandFindAgentByMCPAPIKeyHashis only exercised indirectly. Please add tests for:- happy-path generation returns a 64-hex plaintext and stores a SHA-256 hash;
- regenerating overwrites the old hash (old key no longer authenticates);
- non-ACP agents are rejected;
- project/global scope validation works (project agent via
GetAgent, global agent viaGetGlobalAgent); Authenticateresolves a valid per-agent MCP key to the correctAgentID;- a wrong key still returns
ErrNotFound/ErrInvalidKeyrather than resolving to an agent.
-
Do not swallow non-
ErrNotFounderrors inFindAgentByMCPAPIKeyHash. Currently the repository returnsErrAgentNotFoundfor any error, including a database failure. This can turn a transient DB outage into a misleading 401/404. Return the underlying error unless it issql.ErrNoRows. -
Update
apps/mcp/README.md. The README still describes a singleAGENT_API_KEYand saysPACA_PROJECT_IDis required whenPACA_AGENT_IDis set. After this PR, ACP agents use a per-agentPACA_API_KEYandPACA_PROJECT_IDis optional for global agents. Please rewrite the setup table and the "Agent Mode vs. User Mode" section to match the new flow shown in the UI.
Suggestions (non-blocking)
- Consider adding an explicit "Revoke MCP key" action in the UI/API. Right now the only way to invalidate a key is to generate a new one, which is fine but not obvious to operators.
- The migration comment explains the partial unique index well, but consider adding the same safety note to the domain/repository code so future readers understand why
mcp_api_key_hashis unique only among ACP agents. - In
Authenticate, the MCP key check happens only after the personal key lookup fails. That ordering is fine because personal keys have revocation/expiration semantics that agent keys do not, but consider documenting why agent keys intentionally bypasslast_used_atand expiration checks.
I'll approve once the tests and README are addressed and the error-handling fix is in place.
Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:
apps/mcp/README.md:113(RIGHT) — file not in PR diff
Kimi K2 (free via Pullfrog for OSS) | 𝕏
CI was failing lint on both the web app (biome format) and services/api (gofmt) — no logic changes, formatting only.
There was a problem hiding this comment.
Important
The latest push resolves the prior review's test-coverage and repository error-handling concerns, but apps/mcp/README.md still documents the old shared-key model and needs to be updated before this ships.
Reviewed changes
This incremental review covers the commits since the prior Pullfrog review at 1f0dd8a5.
- Refactored credential generation in
create-agent-dialog.tsxto issue the ACP bridge token and MCP key concurrently viaPromise.allSettled, so agent creation remains fast and a failure in either step does not block the other. - Extracted
generateHashedSecretinagent_service.goand reused it for bridge tokens and MCP keys, removing duplicated random/SHA-256 code. - Added MCP key unit tests for
GenerateAgentMCPKeyandGenerateGlobalAgentMCPKeycovering success, non-ACP rejection, and project/global scope enforcement. - Fixed
FindAgentByMCPAPIKeyHasherror handling so database errors propagate instead of being swallowed asErrAgentNotFound. - Added
Authenticatefallback tests confirming a per-agent MCP key resolves to the correctAgentID, personal keys take precedence, and unknown keys fail closed. - Added integration tests exercising the full per-agent MCP key auth path, including key regeneration invalidating the old key and
X-Agent-IDbeing ignored when a per-agent key is presented. - Added handler tests for
POST /admin/agents/:agentId/mcp-agent-key.
⚠️ Stale MCP README will mislead users
apps/mcp/README.md still describes the old shared AGENT_API_KEY model and states that PACA_PROJECT_ID is required whenever PACA_AGENT_ID is set. After this PR, ACP agents authenticate with a per-agent PACA_API_KEY and global agents work without PACA_PROJECT_ID. Please update the environment-variable table, the "Agent Mode vs. User Mode" section, and any examples that reference the shared key.
Technical details
# Stale MCP README
## Affected sites
- apps/mcp/README.md — env var table still references shared `AGENT_API_KEY`
- apps/mcp/README.md — "Agent Mode vs. User Mode" table still references `Global AGENT_API_KEY from server config`
- apps/mcp/README.md — `PACA_PROJECT_ID` documented as required when `PACA_AGENT_ID` is set
## Required outcome
- README describes the per-agent `PACA_API_KEY` generated via the UI/API.
- README clarifies that `PACA_PROJECT_ID` is optional for global ACP agents.
- README removes references to the server-side shared `AGENT_API_KEY`.
## Suggested approach
Mirror the updated UI copy in `acp-bridge-setup.tsx` and the integration-test scenarios in `services/api/test/integration/agent_apikey_test.go`.Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found. All prior review feedback has been addressed.
Reviewed changes
This incremental review covers the commit since the prior Pullfrog review at 20f7fdee.
- Rewrote
apps/mcp/README.mdto document the per-agentPACA_API_KEYmodel: removed references to the shared server-sideAGENT_API_KEY, clarified thatPACA_PROJECT_IDis optional for global ACP agents, added a global-agent configuration example, and updated the mode table to show agent-global scope. - Hardened
apikeysvc.Authenticateso that onlyErrAgentNotFoundfalls through to the finalErrNotFound; any other error from the MCP-key fallback lookup (e.g. a transient DB failure) now propagates as-is instead of being silently reported as an invalid API key. - Added
TestAuthenticate_AgentStoreDBError_Propagatesto lock in that behavior.
All touched Go unit tests pass (internal/service/agent, internal/service/apikey, internal/repository/postgres, internal/transport/http/handler).
Kimi K2 (free via Pullfrog for OSS) | 𝕏

Summary
Gives each ACP agent its own MCP API key, and overhauls the local-bridge setup guide around it.
Backend — per-agent MCP API key
POST /projects/:projectId/agents/:agentId/mcp-agent-keyand its global-agent siblingPOST /admin/agents/:agentId/mcp-agent-key.FindAgentByMCPAPIKeyHash) instead of relying on the old sharedAGENT_API_KEY+ a client-suppliedX-Agent-IDheader — this closes a cross-agent impersonation gap, since a leaked key can now only ever authenticate as the one agent it was generated for, not any agent in the deployment.000032addsagents.mcp_api_key_hashwith a unique partial index.Frontend — local bridge setup guide
&&-chained line that got too long to read or copy comfortably.claude setup-token→export CLAUDE_CODE_OAUTH_TOKEN=...two-step flow before the run command — the bridge invokesclaudenon-interactively, so the CLI needs a valid token already exported when it starts.OPENAI_API_KEY/GEMINI_API_KEYbefore starting the bridge.