fix(mcp): audit the columns an MCP server update wrote, not the params it got - #6598
Conversation
…s it got Every PATCH /api/mcp/servers/[id] audit row listed oauthClientId, oauthClientIdProvided and oauthClientSecretProvided — on edits that never touched credentials — while omitting the connectionStatus/lastConnected/ lastError resets the write actually performed. The route always sends `oauthClientId: body.oauthClientId || null` and `*Provided: ... !== undefined`, and null and false both survive a `value !== undefined` filter. The two *Provided flags are control params, not columns at all. Only the writer knows which columns a write touched, so updateMcpServer now returns updatedFields from its updateData and both the internal audit wrapper and the v2 use case record it. This matches workflow-mcp-lifecycle and credentials/orchestration, which already report written columns this way.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryLow Risk Overview
Reviewed by Cursor Bugbot for commit 3bf7a0d. Configure here. |
Greptile SummaryThe PR corrects MCP server update audits by deriving
Confidence Score: 5/5The PR appears safe to merge, with the audit metadata now matching the columns written by MCP server updates. The writer derives audit fields from the same update object passed to the database, and both audit paths preserve that result without transformation; no actionable defects remain.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/mcp/orchestration/server-lifecycle.ts | Returns audit field names from the exact object written to the database and forwards them through the internal audit wrapper. |
| apps/sim/lib/mcp/application/use-cases.ts | Replaces input-derived audit metadata with the writer-provided list of updated columns. |
| apps/sim/lib/mcp/orchestration/server-lifecycle.test.ts | Adds regression assertions for rename-only audits and implicit connection-reset columns. |
Sequence Diagram
sequenceDiagram
participant Caller
participant UseCase
participant Writer as updateMcpServer
participant DB
participant Audit
Caller->>UseCase: Update MCP server
UseCase->>Writer: Validated update parameters
Writer->>Writer: Build updateData
Writer->>DB: SET updateData
DB-->>Writer: Updated server
Writer-->>UseCase: server + keys(updateData) excluding updatedAt
UseCase->>Audit: MCP_SERVER_UPDATED with updatedFields
Reviews (1): Last reviewed commit: "fix(mcp): audit the columns an MCP serve..." | Re-trigger Greptile
Summary
PATCH /api/mcp/servers/[id]audit row listedoauthClientId,oauthClientIdProvidedandoauthClientSecretProvided— including renames and URL edits that never touched credentials — and omitted theconnectionStatus/lastConnected/lastErrorresets the write actually performedoauthClientId: body.oauthClientId || nulland*Provided: ... !== undefined, sonullandfalseboth survive thevalue !== undefinedfilter. All three appear on every PATCH regardless of what the client sends — no modal-side condition needed. The two*Providedflags aren't even columns; they're control params263e3ca67e), which splitperformUpdateMcpServerinto a writer that ownsupdateDataand a thin audit wrapper with no access to it.mainderived the list fromObject.keys(updateData); the wrapper fell back to the params it could seeupdateMcpServernow returnsupdatedFieldsand both the internal wrapper and the v2 use case record it. Net −9 lines of derivation logic.map(([key]) => key)records names, never values. No credential value was or is exposedWhy this shape
This is the pattern the repo already uses:
workflow-mcp-lifecycle.ts:96,552,939andcredentials/orchestration/index.ts:159,339both carryupdatedFields?: string[]on the result and populate it from the writer'supdateData. This makesserver-lifecycle.tsthe third instance rather than a new convention. The v2 twin shared the flaw in a different form (Object.keys(input)with no filter at all) and now reads the sameresult.updatedFields.Intentional change to audit output
Rows now include
connectionStatus/lastConnected/lastErroron an auth or credential change, includeauthTypewhen it is implicitly promoted tooauth, and stop includingoauthClientIdProvided/oauthClientSecretProvided. I grepped for downstream consumers keyed on the old strings (EE audit-log UI, v1 export, dashboards) and found none.Type of Change
Testing
server-lifecycle.test.ts: one asserting a rename records exactly['name'], one asserting an auth-type flip records the reset columnsexpected [ 'name', 'oauthClientId', …(2) ] to deeply equal [ 'name' ]andexpected [ 'authType' ] to deeply equal ArrayContaining{…}lib/mcp,app/api/mcp,app/api/v2/mcp,lib/credentialscheck:openapiunchanged andcheck:api-validation:strictpass — confirms noupdatedFieldskey leaks into a wire response (no caller spreads the result; the v2 projection re-wraps picked fields)Follow-ups (not in this PR)
performCreateMcpServer's upsert branch rewrites an existing server (name, URL, headers, OAuth creds, auth-type flip, same connection reset) but gates its audit onif (!result.updated), so an upsert-rewrite emits noMCP_SERVER_UPDATEDrow at all. That's a behavior change, and it needsupdateValuestightened fromRecord<string, unknown>toPartial<typeof mcpServers.$inferInsert>first to be column-safeObject.keys(updateData).filter(k => k !== 'updatedAt')now appears in 6 places across 4 files (5 predate this PR). Worth one shared helper so the exclusion set is a single edit, but that spanslib/credentialsand a v1 admin route — its own PRChecklist