fix(apps): complete MCP connection follow-ups

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-20 21:20:02 +00:00
1 parent ab5e821d6a
commit faaa0ce428
9 files changed
+190 -30

No files matched your search

@@ -5840,18 +5840,38 @@ describeEmbeddedPostgres("tool access service", () => {
const service = toolAccessService(db);
mockToolsList([
{ name: "list_zaps", description: "List", inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: true } },
{ name: "update_zap", description: "Update", inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: false } },
]);
const connect = await service.connectGalleryApp(company.id, {
galleryKey: "zapier",
name: "Zapier reconnect",
credentialValues: { "credentials.authorization": "old-secret" },
}, { actorType: "user", actorId: "board" });
const connect = await withGalleryServerUrl("zapier", PUBLIC_MCP_FIXTURE_URL, () =>
service.connectGalleryApp(company.id, {
galleryKey: "zapier",
name: "Zapier reconnect",
credentialValues: { "credentials.authorization": "old-secret" },
}, { actorType: "user", actorId: "board" }));
const before = await service.getConnection(connect.connectionId, company.id);
const beforeRef = before.credentialSecretRefs.find((r) => r.configPath === "credentials.authorization")!;
expect(beforeRef).toBeDefined();
const listEntry = connect.catalog.find((entry) => entry.toolName === "list_zaps")!;
const updateEntry = connect.catalog.find((entry) => entry.toolName === "update_zap")!;
const finished = await service.finishGalleryAppConnection(company.id, connect.connectionId, {
enabledCatalogEntryIds: [listEntry.id, updateEntry.id],
askFirstCatalogEntryIds: [updateEntry.id],
access: "all_agents",
}, { actorType: "user", actorId: "board" });
await db.delete(toolProfileEntries).where(eq(toolProfileEntries.profileId, finished.profile.id));
await db.update(toolCatalogEntries).set({
status: "quarantined",
quarantineReason: "pending_review",
quarantinedAt: new Date(),
}).where(eq(toolCatalogEntries.connectionId, connect.connectionId));
await db.update(toolConnections).set({
config: { ...before.config, quarantineNewEntries: true },
transportConfig: { ...before.transportConfig, quarantineNewEntries: true },
}).where(eq(toolConnections.id, connect.connectionId));
await expect(
service.reconnectGalleryApp(connect.connectionId, company.id, { credentialValues: {} }, { actorType: "user", actorId: "board" }),
).rejects.toMatchObject({ message: expect.stringContaining("Paste a new key") });
@@ -5869,6 +5889,27 @@ describeEmbeddedPostgres("tool access service", () => {
// Rotated in place: same secret, no duplicate ref created.
expect(after.credentialSecretRefs).toHaveLength(before.credentialSecretRefs.length);
expect(afterRef.secretId).toBe(beforeRef.secretId);
expect(after.config).toMatchObject({ quarantineNewEntries: false });
expect(after.transportConfig).toMatchObject({ quarantineNewEntries: false });
const catalogAfterReconnect = await db.select().from(toolCatalogEntries).where(
eq(toolCatalogEntries.connectionId, connect.connectionId),
);
expect(catalogAfterReconnect).toEqual(expect.arrayContaining([
expect.objectContaining({ id: listEntry.id, status: "active", quarantineReason: null }),
expect.objectContaining({ id: updateEntry.id, status: "active", quarantineReason: null }),
]));
const profileEntriesAfterReconnect = await db.select().from(toolProfileEntries).where(
eq(toolProfileEntries.profileId, finished.profile.id),
);
expect(profileEntriesAfterReconnect).toEqual(expect.arrayContaining([
expect.objectContaining({ catalogEntryId: listEntry.id, effect: "include" }),
expect.objectContaining({ catalogEntryId: updateEntry.id, effect: "include" }),
]));
await expect(db.select().from(toolPolicies).where(and(
eq(toolPolicies.companyId, company.id),
eq(toolPolicies.enabled, true),
))).resolves.toHaveLength(0);
});
it("stops and restarts local stdio runtime slots through the board service", async () => {
+36 -11
View File
@@ -4129,7 +4129,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
}
}
async function refreshCatalog(connectionId: string, actor?: ActorInfo): Promise<ToolCatalogRefreshResult> {
async function refreshCatalog(
connectionId: string,
actor?: ActorInfo,
refreshOptions: { enableAllByDefault?: boolean } = {},
): Promise<ToolCatalogRefreshResult> {
const connection = await getConnectionRow(connectionId);
const now = new Date();
let descriptors: McpToolDescriptor[];
@@ -4161,7 +4165,8 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
const sourceTemplateKey = typeof asRecord(connection.config).sourceTemplateKey === "string"
? String(asRecord(connection.config).sourceTemplateKey)
: null;
const quarantineOnRefresh = shouldQuarantineNewEntries(connection)
const quarantineOnRefresh = !refreshOptions.enableAllByDefault
&& shouldQuarantineNewEntries(connection)
&& (connection.status === "active" || sourceTemplateKey === "posthog");
const safeDefault = asRecord(connection.config).safeDefault === true;
for (const descriptor of descriptors) {
@@ -4239,9 +4244,17 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
}
}
const normalizedConfig = refreshOptions.enableAllByDefault
? { ...connection.config, quarantineNewEntries: false }
: connection.config;
const normalizedTransportConfig = refreshOptions.enableAllByDefault
? { ...connection.transportConfig, quarantineNewEntries: false }
: connection.transportConfig;
const [updatedConnection] = await db
.update(toolConnections)
.set({
config: normalizedConfig,
transportConfig: normalizedTransportConfig,
healthStatus: "ok",
healthMessage: "Tool catalog refreshed.",
healthCheckedAt: now,
@@ -4264,15 +4277,25 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
const activeEntries = updatedEntries.filter((entry) => entry.status === "active");
await enableCatalogEntriesByDefault({
connection: updatedConnection,
newCatalogEntryIds: activeEntries
.filter((entry) => {
const previous = existingByName.get(entry.toolName);
return !previous || previous.status === "quarantined";
})
.map((entry) => entry.id),
newCatalogEntryIds: refreshOptions.enableAllByDefault
? activeEntries.map((entry) => entry.id)
: activeEntries
.filter((entry) => {
const previous = existingByName.get(entry.toolName);
return !previous || previous.status === "quarantined";
})
.map((entry) => entry.id),
activeCatalogEntryIds: activeEntries.map((entry) => entry.id),
actor,
});
if (refreshOptions.enableAllByDefault) {
await upsertAskFirstPolicies({
companyId: updatedConnection.companyId,
connection: updatedConnection,
askFirstEntries: [],
actor,
});
}
await audit({
companyId: connection.companyId,
@@ -6631,7 +6654,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
}
throw error;
}
const refresh = await refreshCatalog(connectionRow.id, actor);
const refresh = await refreshCatalog(connectionRow.id, actor, { enableAllByDefault: true });
const [application] = await db.select().from(toolApplications).where(eq(toolApplications.id, applicationRow.id));
return {
connectionId: refresh.connection.id,
@@ -7072,7 +7095,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
.where(eq(toolConnections.id, connection.id))
.returning();
await syncCredentialBindings(updated);
return checkConnectionHealth(updated.id, actor);
const health = await checkConnectionHealth(updated.id, actor);
const refresh = await refreshCatalog(updated.id, actor, { enableAllByDefault: true });
return { ...health, connection: refresh.connection };
}
async function startOAuth(
@@ -7532,7 +7557,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
await syncCredentialBindings(connection);
await checkConnectionHealth(connection.id, input.actor);
const refresh = await refreshCatalog(connection.id, input.actor);
const refresh = await refreshCatalog(connection.id, input.actor, { enableAllByDefault: true });
const [application] = await db.select().from(toolApplications).where(eq(toolApplications.id, connection.applicationId));
return {
connectionId: refresh.connection.id,