fix: MCP tool confirmation lifecycle, vault token resolution, and dialect-portable SQL - #9
Open
pandacooming wants to merge 3 commits into
Conversation
added 3 commits
April 11, 2026 03:31
Adds a higher-level MCP module (mcp/) that provides: - SSE transport client for persistent MCP server connections - Per-session registry for connection management - Vault-based credential resolution - Executor with tool resolution and confirmation support Also includes: - events.ts: user.tool_confirmation handler (approve/deny) Note: The engine currently uses lib/mcp-client.ts (StreamableHTTP) for MCP integration. This module is available as an alternative implementation and can be integrated into the engine loop in a follow-up PR. Co-authored-by: Claude
…lect-portable SQL
- Add vault_ids to AgentConfig; resolveTools() looks up MCP tokens
from vault (via resolveMCPCredential) before falling back to
mcp_connections table
- MCP tools (both __mcp__ and mcp_ prefixed) now go idle with
evaluated_permission=pending until user sends
user.tool_confirmation {result: allow|deny}
- buildMessagesFromEvents() skips pending tool_use entries so
the LLM won't see them in the message history
- allow confirmation: patch evaluated_permission from pending→allow
using dialect-portable SQL (read → JSON.parse → modify → write;
avoids json_replace which is SQLite-only). Query by tool_use_id
in the JSON data field, not by event id.
- deny confirmation: inject agent.tool_result {is_error: true}
with user's deny_message (or default). Engine skips re-execution
when it sees an existing tool_result for the same tool_use_id
- Export storeEvent from engine/index.ts for test usage
- Add integration tests covering: pending event storage,
allow confirmation, deny with custom message, deny with default
message, and graceful no-op for unknown tool_use_id
- Wire resolveMCPTools/executeMCPTool from mcp/executor into
engine loop: mcp_toolset agent tools use mcp_<server>_<tool>
prefix, coexists with legacy __mcp__<connector>__<tool>
approach
…ages skip-pending tests - mcp/executor.test.ts: 20 tests covering pure functions (isMCPTool, getMCPServerName, mcpToolRequiresConfirmation), resolveMCPTools (mocked registry), and executeMCPTool (mocked registry + callMCPTool) - mcp-sse-roundtrip.test.ts: 7 integration tests proving the SSE MCP client (createMCPServerConnection + discoverMCPTools + callMCPTool) works end-to-end against a real in-process HTTP server that speaks the SSE+JSON-RPC MCP protocol - engine-build-messages.test.ts: 4 tests for buildMessagesFromEvents skip-pending logic — pending tool_use entries are excluded from the LLM message history while allowed/denied ones are included Bugfix (client.ts): startSSEListener was await'ed inside createMCPServerConnection, but SSE responses are long-lived and never "complete", causing the connection to deadlock indefinitely. Changed to fire-and-forget with a .catch() error handler. Also exports buildMessagesFromEvents from engine/index.ts so it can be unit-tested directly.
pandacooming
force-pushed
the
fix/mcp-tool-confirmation-vault-credentials
branch
from
April 11, 2026 03:32
16bfc8c to
672f7d1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to PR #8, addressing the review feedback: the MCP tool confirmation flow and vault token resolution are now fully wired into the engine, with dialect-portable SQL and matching test coverage.
What was left on the table in #8, and what this PR does about it
AgentConfigdoesn't declarevault_ids;resolveTools()never reads itvault_ids?: string[]toAgentConfig;resolveTools()passes it to credential resolutionjson_replaceis SQLite-only; Postgres CI job would breakJSON.parse→ modify →JSON.stringify— works on both SQLite and Postgresevaluated_permissionevaluated_permission === "pending"; re-execution loop also skips any tool that already has atool_resultwith matchingtool_use_idagent.tool_result; engine's re-execution loop detects it viaexistingResultRowfull-scan and skipsmcp/module is unwired — don't merge parallel implementationsmcp/executor(resolveMCPTools+executeMCPTool) is now wired into the engine loop;mcp_toolsetagent entries usemcp_<server>_<tool>namingChanges
1.
mcp/module wired into engine loop (engine/index.ts, new files)The
mcp/module (SSE transport + per-session registry + vault-based credential resolution + executor) from the previous PR is now called by the engine:resolveMCPToolsresolves tools formcp_toolsetagent config entriesexecuteMCPToolexecutesmcp_<server>_<tool>calls with session-scoped MCP connections__mcp__<connector>__<tool>routing (StreamableHTTP vialib/mcp-client.ts)New files:
mcp/client.ts,mcp/executor.ts,mcp/registry.ts,mcp/types.ts,mcp/index.ts(1087 lines)2. Vault token resolution (
engine/index.ts)AgentConfignow includesvault_ids?: string[]resolveTools()passesvaultIdsto credential resolution; MCP tokens are fetched from vault viaresolveMCPCredentialbefore falling back to themcp_connectionstable3. Tool confirmation lifecycle (
engine/index.ts,routes/events.ts)__mcp__andmcp_prefixed) setevaluated_permission=pendinginagent.tool_useevents and go idlebuildMessagesFromEvents()skipspendingtool_use entries so the LLM doesn't see them in historyevaluated_permissionfrompending→allowusing dialect-portable SQL; query finds the event bytool_use_idin the JSON data field (not by event id)agent.tool_result {is_error: true}withdeny_message; engine skips re-execution when it detects an existing result for thattool_use_idstoreEventandbuildMessagesFromEventsexported fromengine/index.tsfor test usage4. Confirmation integration tests (
sessions.test.ts) — updatedReplaced 2 stop-session tests with 5 new confirmation lifecycle tests:
agent.tool_useevent is stored correctlyallowconfirmation updatesevaluated_permissiontoallowdenywith custom message injects the correct error contentdenywith no message uses the default denial texttool_use_idis a silent no-op (200, not 404)5.
mcp/executor.tsunit tests (mcp-executor.test.ts, new)20 tests covering:
isMCPTool/getMCPServerName/mcpToolRequiresConfirmation(pure functions)resolveMCPToolswith mocked registry: server connection, tool prefixing, default/toolset perms, error fallback, uninitialized skipexecuteMCPToolwith mocked registry: correct routing, invalid format, disconnected server, uninitialized server, error propagation6. SSE MCP round-trip integration test (
mcp-sse-roundtrip.test.ts, new)7 tests proving the
mcp/client.tsSSE MCP client works against a real in-process HTTP server:createMCPServerConnection+discoverMCPToolsreturnsmcp_<server>_...prefixed toolscallMCPToolexecutes echo / add / uppercase via SSE and returns correct resultsis_error: trueIncludes a bugfix:
startSSEListenerwasawait'd insidecreateMCPServerConnection, but SSE responses are long-lived and never "complete", causing the call to deadlock indefinitely. Changed to fire-and-forget with a.catch()error handler.7.
buildMessagesFromEventsskip-pending unit tests (engine-build-messages.test.ts, new)4 tests proving the skip logic:
evaluated_permission=pendingentries are excluded from LLM historyevaluated_permission=allowentries are includedagent.tool_result(denial) is always included regardless of permission stateTest results