fix(mcp): audit an upsert that rewrites or revives a server - #6602
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Upsert results now expose Reviewed by Cursor Bugbot for commit 47daecf. Configure here. |
Greptile SummaryThis follow-up completes audit coverage for MCP server upserts while redacting sensitive URL components from MCP audit metadata.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/mcp/application/use-cases.ts | Classifies registration rewrites and revivals for semantic auditing and sanitizes URL metadata across use-case audit paths. |
| apps/sim/lib/mcp/orchestration/server-lifecycle.ts | Surfaces revival and written-field metadata from upserts, records the corresponding audit action, and sanitizes legacy lifecycle audit URLs. |
| apps/sim/lib/mcp/orchestration/server-lifecycle.test.ts | Adds regression tests for rewrite and revival audit actions, updated fields, and query-token redaction. |
Sequence Diagram
sequenceDiagram
participant Caller
participant UseCase as MCP use case
participant Lifecycle as Server lifecycle
participant DB
participant Audit
Caller->>UseCase: Register MCP server URL
UseCase->>Lifecycle: createMcpServer(...)
Lifecycle->>DB: Find row by generated server ID
alt Existing live row
Lifecycle->>DB: Rewrite server configuration
Lifecycle-->>UseCase: "updated=true, revived=false, updatedFields"
UseCase->>Audit: MCP_SERVER_UPDATED with sanitized URL
else Existing soft-deleted row
Lifecycle->>DB: Revive server row
Lifecycle-->>UseCase: "updated=true, revived=true"
UseCase->>Audit: MCP_SERVER_ADDED with sanitized URL
else New row
Lifecycle->>DB: Insert server
Lifecycle-->>UseCase: "updated=false"
UseCase->>Audit: MCP_SERVER_ADDED with sanitized URL
end
Reviews (2): Last reviewed commit: "fix(mcp): redact audit URLs and drop unw..." | Re-trigger Greptile
Registering a URL that already exists takes the upsert branch and rewrites the live row — name, transport, headers, timeout, enabled, auth type, the connection reset, and the URL's query string, since the server id hashes only origin and pathname. That branch recorded no audit row at all: the ADDED audit was gated on `!result.updated`. main recorded ADDED for these (wrong action, but a row existed), so this restores coverage and fixes the action. Reachable from the settings POST /api/mcp/servers and from Copilot's manage_mcp_tool `add`, neither of which passes existingServerBehavior. The v2 POST passes 'reject' so it only reaches the upsert on a revival. A rewrite is now MCP_SERVER_UPDATED carrying updatedFields; a revival of a soft-deleted row stays MCP_SERVER_ADDED. updateValues is typed Partial<$inferInsert> so Object.keys is column-safe. Analytics gating is unchanged: mcp_server_connected still fires only for a genuine insert.
…elds Two review findings on the new upsert audit. The upsert assigns every column unconditionally, so `description` is present on updateValues but undefined when the registration omits it. Drizzle skips undefined in .set(), so deriving keys without checking values made the audit claim a column the write never touched. Filter by value; null stays, since clearing a value is a write. MCP URLs carry tokens in their query string — that is why a silent rewrite of one matters — and audit rows are readable by org admins who need no workspace MCP access. Newly auditing rewrites would persist those tokens verbatim, so every MCP audit row now records the URL through sanitizeUrlForLog, which strips query and fragment. Applied to the add, update and delete rows alike: redacting only the new path would leave the same credential in the row a first registration already writes. A null url stays null rather than becoming an empty string.
55827c3 to
47daecf
Compare
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 47daecf. Configure here.
| */ | ||
| updatedFields = Object.entries(updateValues) | ||
| .filter(([key, value]) => key !== 'updatedAt' && value !== undefined) | ||
| .map(([key]) => key) |
There was a problem hiding this comment.
Rewrite audit lists soft-delete field
Low Severity
Live-rewrite updatedFields always includes deletedAt because the upsert SET writes deletedAt: null and the new filter keeps nulls. Revival is audited as MCP_SERVER_ADDED without updatedFields, so the soft-delete column only appears when it was already null and nothing undeleted.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 47daecf. Configure here.


Summary
!result.updatedmainrecordedMCP_SERVER_ADDEDfor these unconditionally: wrong action, but a row existed. improvement(external-endpoints): v2 versions with clean signatures + updated docs based on openapi spec #5273 added the gate, so staging records nothing. This is an audit-coverage regression, the third traced to that PRMCP_SERVER_UPDATEDcarryingupdatedFields; reviving a soft-deleted row staysMCP_SERVER_ADDEDupdatedFieldson the resultWhat the upsert silently rewrites
name, description, transport, headers, timeout, retries, enabled, authType, the connection reset — and the URL's query string.
generateMcpServerIdhashesorigin + pathnameonly (utils.ts:238strips query and fragment), so re-registering…/mcp?token=NEWover…/mcp?token=OLDlands on the same row and repoints it. MCP endpoints commonly carry tokens there, and headers carry auth.Reachability
existingServerBehaviorPOST /api/mcp/servers(settings "add server")manage_mcp_toolopadd→registerMcpServerUseCasePOST /api/v2/mcp-servers→createMcpServerUseCase'reject'The Copilot path is what motivated fixing this now: an agent can rewrite a server's headers and URL query with no audit trail.
Two distinct missing rows
createMcpServerUseCasethrows unlessidState.deleted, so v2's onlyupdatedcase is a revival — it was dropping a legitimate ADDED row on every revival. Live rewrites and revivals need different actions, so the orchestration now surfaces theisRevivalit already computed asrevivedon the result.Type of Change
Testing
MCP_SERVER_UPDATEDwithupdatedFieldscontainingurlandheaders; a soft-deleted revival assertsMCP_SERVER_ADDEDwith noupdatedFieldsexpected undefined to be 'mcp_server.updated'/'mcp_server.added'— i.e. no row at alllib/mcp,app/api/mcp,app/api/v2/mcpupdateValuesretyped fromRecord<string, unknown>toPartial<typeof mcpServers.$inferInsert>; typecheck passing is the proof every key is a real column, which is what makesObject.keyssafe herelint:check,check:api-validation:strict,check:openapiall passDeliberately unchanged
Analytics gating.
mcp_server_connectedstill fires only for a genuine insert — a revival arguably counts as a new connection, but changing it would skewfirst_mcp_connected_atand is a separate decision.Checklist