diff --git a/packages/shared/src/validators/tool-access.ts b/packages/shared/src/validators/tool-access.ts index 914af74e7ab..f55163c22eb 100644 --- a/packages/shared/src/validators/tool-access.ts +++ b/packages/shared/src/validators/tool-access.ts @@ -191,7 +191,8 @@ export const createToolConnectionSchema = z.object({ status: toolConnectionStatusSchema.optional(), connectionKind: toolConnectionKindSchema.default("managed"), config: toolTransportConfigSchema.optional(), - transportConfig: toolTransportConfigSchema.default({}), + // No default: when the caller omits it, the service stores `config` here. + transportConfig: toolTransportConfigSchema.optional(), credentialRefs: z.array(mcpConnectionCredentialRefSchema).optional(), credentialSecretRefs: z.array(toolCredentialSecretRefSchema).default([]), enabled: z.boolean().optional(), diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index 186b20e3042..69525cf08e0 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -68,6 +68,7 @@ import { GOOGLE_WORKSPACE_DEFAULT_SELECTION, getAvailableConnectionMethod, getConnectableAppDefinition, + createToolConnectionSchema, type CreateToolConnection, type GoogleWorkspaceConnectorProfileId, } from "@paperclipai/shared"; @@ -3363,6 +3364,42 @@ describeEmbeddedPostgres("tool access service", () => { .where(eq(companySecrets.companyId, company.id))).toEqual([]); }); + it.each([ + { name: "channel purpose", input: { connectionPurpose: "channel" as const, transport: "local_stdio" as const } }, + { name: "chat transport", input: { transport: "chat_sdk" as const } }, + { name: "voice transport", input: { transport: "voice" as const } }, + ])("rejects a $name before it writes any row", async ({ input }) => { + const company = await createCompany(db); + const service = createTestToolAccessService(db); + + await expect(service.createConnection(company.id, connectionInput({ + name: "Not a tool", + config: { templateId: "paperclip.echo-calculator-time" }, + ...input, + }))).rejects.toMatchObject({ status: 422 }); + + expect(await db.select().from(toolConnections) + .where(eq(toolConnections.companyId, company.id))).toEqual([]); + expect(await db.select().from(toolApplications) + .where(eq(toolApplications.companyId, company.id))).toEqual([]); + }); + + it("stores config as transportConfig when the request leaves transportConfig out", async () => { + const company = await createCompany(db); + const service = createTestToolAccessService(db); + const body = createToolConnectionSchema.parse({ + name: "Local echo config only", + transport: "local_stdio", + config: { templateId: "paperclip.echo-calculator-time" }, + }); + + const connection = await service.createConnection(company.id, body); + + const [stored] = await db.select().from(toolConnections).where(eq(toolConnections.id, connection.id)); + expect(stored?.config).toMatchObject({ templateId: "paperclip.echo-calculator-time" }); + expect(stored?.transportConfig).toEqual(stored?.config); + }); + it("registers an approved local stdio template and exposes its runtime slot", async () => { const company = await createCompany(db); const service = createTestToolAccessService(db); diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index 7441bcee493..ae1682df28c 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -4530,6 +4530,13 @@ export function toolAccessService( let applicationNamespace = input.applicationName ?? input.name; const transport = input.transport; if (!transport) throw badRequest("Tool connection transport is required"); + // This path writes only tool connections. Channel setup has its own + // services. Reject other purposes here, before any row is written, so the + // database purpose/transport check never turns them into a 500. + if (input.connectionPurpose !== undefined && input.connectionPurpose !== "tool") + throw unprocessable("Only tool connections can be created here"); + if (transport === "chat_sdk" || transport === "voice") + throw unprocessable("Chat and voice connections cannot be created here"); const config = normalizeGoogleSheetsConnectionConfig( input.config ?? input.transportConfig ?? {}, );