mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(apps): complete MCP connection follow-ups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user