Skip to content

feat(mcp): add policy-gated MCP bridge with audit pipeline#10

Open
dubscode wants to merge 1 commit intofeat/context-graph-enrichmentfrom
feat/mcp-bridge-and-spec-archive
Open

feat(mcp): add policy-gated MCP bridge with audit pipeline#10
dubscode wants to merge 1 commit intofeat/context-graph-enrichmentfrom
feat/mcp-bridge-and-spec-archive

Conversation

@dubscode
Copy link
Contributor

@dubscode dubscode commented Mar 5, 2026


🥞 DubStack

Implement discover/connect/execute bridge contracts, runtime wiring, default adapter,
tests, and rollout docs. Archive full-mcp-tool-bridge and sync delta specs into
openspec/specs.
@github-actions
Copy link

github-actions bot commented Mar 5, 2026

PR Checks Summary

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a policy-gated MCP “bridge” layer (discover/connect/execute) with normalized envelopes and an audit pipeline, wiring it into the CLI runtime and documenting rollout/specs.

Changes:

  • Introduces McpBridgeService with correlation IDs, standardized error classification, policy enforcement, and audit event emission.
  • Adds a default MCP server adapter + env-driven server config loader (DUBSBOT_MCP_SERVERS_JSON) and wires bridge auditing into TraceStore.
  • Adds unit/integration tests plus rollout documentation and OpenSpec artifacts (including archiving the original change plan).

Reviewed changes

Copilot reviewed 14 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/mcp-bridge.test.ts Unit tests for discovery sorting, connection failure normalization, policy denial short-circuit, session reuse health checks, and audit emission.
tests/integration/mcp-bridge.integration.test.ts End-to-end discover/connect/execute flow verifying correlation propagation and envelope normalization.
src/mcp/default-adapter.ts Default adapter implementation for configured MCP servers (discovery/connect/invoke) backed by McpClient.
src/mcp/bridge.ts Core bridge service/types: normalized contracts, policy gate, session cache, error classification, payload redaction/summary, audit events.
src/config/mcp-loader.ts Loads MCP server configs from DUBSBOT_MCP_SERVERS_JSON using Zod.
src/cli/runtime.ts Wires MCP bridge into runtime and routes audit events into TraceStore (mcp.bridge.*).
openspec/specs/mcp-tool-execution-pipeline/spec.md Adds spec for standardized execution pipeline and error mapping.
openspec/specs/mcp-server-discovery/spec.md Adds spec for deterministic discovery and diagnostics.
openspec/specs/mcp-server-connection/spec.md Adds spec for explicit connect lifecycle and failure classification.
openspec/specs/mcp-policy-and-audit/spec.md Adds spec for policy enforcement and append-only audit events with timing.
openspec/changes/full-mcp-tool-bridge/tasks.md Removes the in-progress tasks checklist (moved to archive).
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/tasks.md Archived, completed checklist for the MCP bridge change.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/specs/mcp-tool-execution-pipeline/spec.md Archived “added requirements” snapshot.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/specs/mcp-server-discovery/spec.md Archived “added requirements” snapshot.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/specs/mcp-server-connection/spec.md Archived “added requirements” snapshot.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/specs/mcp-policy-and-audit/spec.md Archived “added requirements” snapshot.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/proposal.md Archived proposal rationale/scope/impact for the change.
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/design.md Archived design doc (goals, decisions, risks, migration plan).
openspec/changes/archive/2026-03-04-full-mcp-tool-bridge/.openspec.yaml Archived OpenSpec metadata for the change.
docs/mcp-bridge-rollout.md Documents bridge contracts, audit schema, env config, and rollout/rollback steps.
README.md Updates repo overview/config docs to mention MCP bridge and env var + rollout guide.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +83 to +108
this.sessions.set(session.id, { session, healthy: true });
return session;
}

async isSessionHealthy(session: McpBridgeSession): Promise<boolean> {
const state = this.sessions.get(session.id);
return state?.healthy ?? false;
}

async invoke(
session: McpBridgeSession,
toolName: string,
input: Record<string, unknown>
): Promise<McpInvokeResult> {
const state = this.sessions.get(session.id);
if (!state || !state.healthy) {
throw new Error('Connection failed: session is unavailable');
}
const server = this.servers.get(session.serverId);
if (!server) {
throw new Error('Connection failed: server missing for active session');
}

if (server.transport !== 'stdio') {
throw new Error(`Tool invocation failed: unsupported transport "${server.transport}"`);
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect() returns a successful session for non-stdio transports, but invoke() later throws unsupported transport for anything other than stdio. This makes discovery/connect report a server as available/connected even though it can never execute tools. Either implement invocation for http/sse, or treat non-stdio transports as misconfigured/unavailable and fail connect() (and/or surface diagnostics in discover()) until supported.

Copilot uses AI. Check for mistakes.
try {
const parsed = JSON.parse(raw) as unknown;
return McpServerConfigListSchema.parse(parsed);
} catch {
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadMcpServerConfig() silently returns [] on JSON/schema parse failures. That makes MCP unexpectedly “disappear” with no diagnostic, and it’s inconsistent with other env config loaders like loadEmbeddingStrategyConfig() which throw actionable errors on invalid JSON. Consider surfacing a warning/trace (or throwing) when DUBSBOT_MCP_SERVERS_JSON is set but invalid, so misconfiguration is detectable.

Suggested change
} catch {
} catch (err) {
console.error(
'[loadMcpServerConfig] Failed to parse DUBSBOT_MCP_SERVERS_JSON; MCP servers will be disabled.',
err,
);

Copilot uses AI. Check for mistakes.
]
```

Invalid configurations are skipped from active use and surfaced as `unavailable` discovery records with `misconfigured` diagnostics.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rollout guide says invalid configurations are surfaced as unavailable discovery records with misconfigured diagnostics, but if DUBSBOT_MCP_SERVERS_JSON is invalid JSON or fails schema parsing, loadMcpServerConfig() currently returns an empty list and discovery will return no records (no diagnostics). Consider clarifying the doc (or changing the loader behavior) so operators know what to expect when the env var is malformed.

Suggested change
Invalid configurations are skipped from active use and surfaced as `unavailable` discovery records with `misconfigured` diagnostics.
If `DUBSBOT_MCP_SERVERS_JSON` is missing or malformed (e.g., invalid JSON or fails overall schema parsing), the loader returns no servers and discovery returns an empty list with no diagnostics. Once the env var is successfully parsed, invalid per-server configurations are skipped from active use and surfaced as `unavailable` discovery records with `misconfigured` diagnostics.

Copilot uses AI. Check for mistakes.
}

private async emitAudit(event: McpAuditEvent): Promise<void> {
await this.deps.auditSink?.append(event);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emitAudit() awaits the sink directly; if the audit sink throws (e.g., TraceStore filesystem error or JSON serialization failure), it will cause discover/connect/execute to throw or fail even when the core operation succeeded. Consider making audit emission best-effort (catch and swallow/log sink errors) so observability cannot break the bridge contract.

Suggested change
await this.deps.auditSink?.append(event);
try {
await this.deps.auditSink?.append(event);
} catch (err) {
// Best-effort: log and swallow audit sink errors so they don't break bridge operations
console.error('Failed to emit MCP audit event:', err);
}

Copilot uses AI. Check for mistakes.
Comment on lines +496 to +505
const policyDecision = this.deps.policyEngine.evaluateToolInvocation({
invocation: {
tool: `mcp:${request.serverId}:${request.toolName}`,
params: request.input,
sideEffect: request.sideEffect ?? 'network',
policyTag: 'mcp',
},
mode: request.mode ?? 'interactive',
approvalGranted: request.approvalGranted ?? false,
});
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() calls policyEngine.evaluateToolInvocation() outside of any try/catch. DefaultPolicyEngine.evaluateToolInvocation() builds a command string using JSON.stringify(invocation.params), which will throw for circular structures or some values (e.g., BigInt). That would make execute() throw without returning a normalized envelope or emitting an audit event. Wrap policy evaluation in a try/catch and map failures into a stable validation/internal error envelope + audit record.

Suggested change
const policyDecision = this.deps.policyEngine.evaluateToolInvocation({
invocation: {
tool: `mcp:${request.serverId}:${request.toolName}`,
params: request.input,
sideEffect: request.sideEffect ?? 'network',
policyTag: 'mcp',
},
mode: request.mode ?? 'interactive',
approvalGranted: request.approvalGranted ?? false,
});
let policyDecision: ApprovalDecision;
try {
policyDecision = this.deps.policyEngine.evaluateToolInvocation({
invocation: {
tool: `mcp:${request.serverId}:${request.toolName}`,
params: request.input,
sideEffect: request.sideEffect ?? 'network',
policyTag: 'mcp',
},
mode: request.mode ?? 'interactive',
approvalGranted: request.approvalGranted ?? false,
});
} catch (_err) {
const endedAt = this.now();
const error: McpBridgeError = {
category: 'validation',
message: 'Failed to evaluate policy for tool invocation',
retryable: false,
code: 'policy_evaluation_failed',
};
const envelope: McpExecutionEnvelope = {
ok: false,
correlationId,
serverId: request.serverId,
toolName: request.toolName,
outputSummary: '',
policyDecision: null,
error,
timing: {
startedAt: startedAt.toISOString(),
endedAt: endedAt.toISOString(),
durationMs: durationMs(startedAt, endedAt),
},
};
await this.emitAudit({
operation: 'execute',
outcome: 'failure',
correlationId,
serverId: request.serverId,
toolName: request.toolName,
error,
metadata: {
input: summarizePayload(request.input),
},
timing: envelope.timing,
});
return envelope;
}

Copilot uses AI. Check for mistakes.
Comment on lines +613 to +617
metadata: {
input: summarizePayload(request.input),
output: envelope.outputSummary,
...invokeResult.metadata,
},
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success audit metadata spreads ...invokeResult.metadata directly into the event payload. If adapter metadata contains non-JSON-serializable values (or large/sensitive blobs), TraceStore’s JSON.stringify will throw or produce oversized logs, and because audit emission is awaited this can break the execution flow. Consider sanitizing/serializing adapter metadata (e.g., run through summarizePayload/redactValue, or only allow primitive/JSON-safe metadata) before appending.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants