@shipfox/api-integration-core 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +264 -0
- package/LICENSE +21 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-tool-selection.d.ts +15 -0
- package/dist/core/agent-tool-selection.d.ts.map +1 -0
- package/dist/core/agent-tool-selection.js +48 -0
- package/dist/core/agent-tool-selection.js.map +1 -0
- package/dist/core/entities/connection.d.ts +2 -0
- package/dist/core/entities/connection.d.ts.map +1 -0
- package/dist/core/entities/connection.js +3 -0
- package/dist/core/entities/connection.js.map +1 -0
- package/dist/core/entities/provider.d.ts +6 -0
- package/dist/core/entities/provider.d.ts.map +1 -0
- package/dist/core/entities/provider.js +3 -0
- package/dist/core/entities/provider.js.map +1 -0
- package/dist/core/errors.d.ts +29 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/providers/agent-tools.d.ts +2 -0
- package/dist/core/providers/agent-tools.d.ts.map +1 -0
- package/dist/core/providers/agent-tools.js +3 -0
- package/dist/core/providers/agent-tools.js.map +1 -0
- package/dist/core/providers/redact-checkout-spec.d.ts +9 -0
- package/dist/core/providers/redact-checkout-spec.d.ts.map +1 -0
- package/dist/core/providers/redact-checkout-spec.js +25 -0
- package/dist/core/providers/redact-checkout-spec.js.map +1 -0
- package/dist/core/providers/registry.d.ts +9 -0
- package/dist/core/providers/registry.d.ts.map +1 -0
- package/dist/core/providers/registry.js +52 -0
- package/dist/core/providers/registry.js.map +1 -0
- package/dist/core/providers/source-control.d.ts +2 -0
- package/dist/core/providers/source-control.d.ts.map +1 -0
- package/dist/core/providers/source-control.js +3 -0
- package/dist/core/providers/source-control.js.map +1 -0
- package/dist/core/source-control-service.d.ts +46 -0
- package/dist/core/source-control-service.d.ts.map +1 -0
- package/dist/core/source-control-service.js +84 -0
- package/dist/core/source-control-service.js.map +1 -0
- package/dist/db/connections.d.ts +67 -0
- package/dist/db/connections.d.ts.map +1 -0
- package/dist/db/connections.js +130 -0
- package/dist/db/connections.js.map +1 -0
- package/dist/db/db.d.ts +865 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +22 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/connections.d.ts +165 -0
- package/dist/db/schema/connections.d.ts.map +1 -0
- package/dist/db/schema/connections.js +37 -0
- package/dist/db/schema/connections.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/webhook-deliveries.d.ts +76 -0
- package/dist/db/schema/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/schema/webhook-deliveries.js +22 -0
- package/dist/db/schema/webhook-deliveries.js.map +1 -0
- package/dist/db/webhook-deliveries.d.ts +55 -0
- package/dist/db/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/webhook-deliveries.js +132 -0
- package/dist/db/webhook-deliveries.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +8 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +17 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +36 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +26 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts +24 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js +52 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts +7 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js +108 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts +12 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js +46 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts +17 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js +126 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +37 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +145 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +53 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +14 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +26 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-connections.d.ts +3 -0
- package/dist/presentation/routes/list-connections.d.ts.map +1 -0
- package/dist/presentation/routes/list-connections.js +58 -0
- package/dist/presentation/routes/list-connections.js.map +1 -0
- package/dist/presentation/routes/list-providers.d.ts +3 -0
- package/dist/presentation/routes/list-providers.d.ts.map +1 -0
- package/dist/presentation/routes/list-providers.js +27 -0
- package/dist/presentation/routes/list-providers.js.map +1 -0
- package/dist/presentation/routes/list-repositories.d.ts +3 -0
- package/dist/presentation/routes/list-repositories.d.ts.map +1 -0
- package/dist/presentation/routes/list-repositories.js +41 -0
- package/dist/presentation/routes/list-repositories.js.map +1 -0
- package/dist/presentation/routes/manage-connections.d.ts +4 -0
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -0
- package/dist/presentation/routes/manage-connections.js +81 -0
- package/dist/presentation/routes/manage-connections.js.map +1 -0
- package/dist/providers/connection-slug.d.ts +2 -0
- package/dist/providers/connection-slug.d.ts.map +1 -0
- package/dist/providers/connection-slug.js +19 -0
- package/dist/providers/connection-slug.js.map +1 -0
- package/dist/providers/cron.d.ts +3 -0
- package/dist/providers/cron.d.ts.map +1 -0
- package/dist/providers/cron.js +13 -0
- package/dist/providers/cron.js.map +1 -0
- package/dist/providers/gitea.d.ts +3 -0
- package/dist/providers/gitea.d.ts.map +1 -0
- package/dist/providers/gitea.js +75 -0
- package/dist/providers/gitea.js.map +1 -0
- package/dist/providers/github.d.ts +3 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/linear.d.ts +3 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +134 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/modules.d.ts +3 -0
- package/dist/providers/modules.d.ts.map +1 -0
- package/dist/providers/modules.js +26 -0
- package/dist/providers/modules.js.map +1 -0
- package/dist/providers/sentry.d.ts +3 -0
- package/dist/providers/sentry.d.ts.map +1 -0
- package/dist/providers/sentry.js +83 -0
- package/dist/providers/sentry.js.map +1 -0
- package/dist/providers/types.d.ts +58 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/webhook.d.ts +3 -0
- package/dist/providers/webhook.d.ts.map +1 -0
- package/dist/providers/webhook.js +24 -0
- package/dist/providers/webhook.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts +4 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js +11 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js.map +1 -0
- package/dist/temporal/constants.d.ts +3 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +4 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js +14 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +41 -0
- package/drizzle/meta/0001_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +85 -0
- package/src/config.test.ts +32 -0
- package/src/config.ts +28 -0
- package/src/core/agent-tool-selection.test.ts +137 -0
- package/src/core/agent-tool-selection.ts +80 -0
- package/src/core/entities/connection.ts +4 -0
- package/src/core/entities/provider.ts +21 -0
- package/src/core/errors.ts +55 -0
- package/src/core/providers/agent-tools.ts +10 -0
- package/src/core/providers/redact-checkout-spec.test.ts +78 -0
- package/src/core/providers/redact-checkout-spec.ts +20 -0
- package/src/core/providers/registry.test.ts +138 -0
- package/src/core/providers/registry.ts +90 -0
- package/src/core/providers/source-control.ts +17 -0
- package/src/core/source-control-service.test.ts +230 -0
- package/src/core/source-control-service.ts +161 -0
- package/src/db/connections.test.ts +296 -0
- package/src/db/connections.ts +262 -0
- package/src/db/db.ts +22 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/connections.ts +49 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/webhook-deliveries.ts +19 -0
- package/src/db/webhook-deliveries.test.ts +267 -0
- package/src/db/webhook-deliveries.ts +207 -0
- package/src/index.ts +212 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +35 -0
- package/src/presentation/dto/integrations.ts +39 -0
- package/src/presentation/routes/agent-tools-gateway/agent-tools-gateway.route.test.ts +371 -0
- package/src/presentation/routes/agent-tools-gateway/audit.test.ts +86 -0
- package/src/presentation/routes/agent-tools-gateway/audit.ts +90 -0
- package/src/presentation/routes/agent-tools-gateway/dispatch.ts +139 -0
- package/src/presentation/routes/agent-tools-gateway/index.ts +62 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.test.ts +308 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.ts +155 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +181 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +245 -0
- package/src/presentation/routes/errors.ts +63 -0
- package/src/presentation/routes/index.ts +53 -0
- package/src/presentation/routes/list-connections.test.ts +227 -0
- package/src/presentation/routes/list-connections.ts +56 -0
- package/src/presentation/routes/list-providers.test.ts +113 -0
- package/src/presentation/routes/list-providers.ts +28 -0
- package/src/presentation/routes/list-repositories.test.ts +150 -0
- package/src/presentation/routes/list-repositories.ts +47 -0
- package/src/presentation/routes/manage-connections.test.ts +125 -0
- package/src/presentation/routes/manage-connections.ts +77 -0
- package/src/providers/connection-slug.ts +19 -0
- package/src/providers/cron.ts +13 -0
- package/src/providers/gitea.ts +103 -0
- package/src/providers/github.ts +150 -0
- package/src/providers/linear.ts +179 -0
- package/src/providers/modules.test.ts +40 -0
- package/src/providers/modules.ts +32 -0
- package/src/providers/sentry.ts +113 -0
- package/src/providers/types.ts +52 -0
- package/src/providers/webhook.ts +30 -0
- package/src/temporal/activities/index.ts +7 -0
- package/src/temporal/activities/prune-webhook-deliveries.test.ts +39 -0
- package/src/temporal/activities/prune-webhook-deliveries.ts +9 -0
- package/src/temporal/constants.ts +3 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/prune-webhook-deliveries-cron.ts +15 -0
- package/test/agent-tools-gateway-helpers.ts +210 -0
- package/test/env.ts +23 -0
- package/test/factories/connection.ts +21 -0
- package/test/globalSetup.ts +22 -0
- package/test/index.ts +1 -0
- package/test/route-utils.ts +104 -0
- package/test/setup.ts +19 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import type {Transport} from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
+
import {AUTH_LEASED_JOB, requireLeasedJobContext} from '@shipfox/api-auth-context';
|
|
4
|
+
import {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
|
|
5
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
6
|
+
import type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
|
|
7
|
+
import {createIntegrationToolCallRecorder} from './audit.js';
|
|
8
|
+
import {createIntegrationToolDispatcher} from './dispatch.js';
|
|
9
|
+
import {buildAgentToolsMcpServer} from './mcp-server.js';
|
|
10
|
+
import {
|
|
11
|
+
type LeasedAgentStepLoader,
|
|
12
|
+
resolveAuthorizedIntegrationTools,
|
|
13
|
+
} from './resolve-authorized-tools.js';
|
|
14
|
+
|
|
15
|
+
export type {LeasedAgentStepLoader} from './resolve-authorized-tools.js';
|
|
16
|
+
|
|
17
|
+
export interface CreateAgentToolsGatewayRoutesParams {
|
|
18
|
+
loadLeasedAgentStep: LeasedAgentStepLoader;
|
|
19
|
+
registry: IntegrationProviderRegistry;
|
|
20
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createAgentToolsGatewayRoutes(
|
|
24
|
+
params: CreateAgentToolsGatewayRoutesParams,
|
|
25
|
+
): RouteGroup {
|
|
26
|
+
const dispatchIntegrationToolCall = createIntegrationToolDispatcher({registry: params.registry});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
prefix: '/runs/jobs/current/integration-tools',
|
|
30
|
+
auth: AUTH_LEASED_JOB,
|
|
31
|
+
routes: [
|
|
32
|
+
defineRoute({
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/mcp',
|
|
35
|
+
description: 'Gateway MCP endpoint for integration-backed agent tools',
|
|
36
|
+
handler: async (request, reply) => {
|
|
37
|
+
const authorizedTools = await resolveAuthorizedIntegrationTools({
|
|
38
|
+
request,
|
|
39
|
+
loadLeasedAgentStep: params.loadLeasedAgentStep,
|
|
40
|
+
registry: params.registry,
|
|
41
|
+
getIntegrationConnectionById: params.getIntegrationConnectionById,
|
|
42
|
+
});
|
|
43
|
+
const server = buildAgentToolsMcpServer({
|
|
44
|
+
authorizedTools,
|
|
45
|
+
dispatch: dispatchIntegrationToolCall,
|
|
46
|
+
recordCall: createIntegrationToolCallRecorder(requireLeasedJobContext(request)),
|
|
47
|
+
});
|
|
48
|
+
const transport = new StreamableHTTPServerTransport();
|
|
49
|
+
|
|
50
|
+
await server.connect(transport as unknown as Transport);
|
|
51
|
+
reply.raw.on('close', () => {
|
|
52
|
+
void transport.close();
|
|
53
|
+
void server.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
reply.hijack();
|
|
57
|
+
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import {InMemoryTransport} from '@modelcontextprotocol/sdk/inMemory.js';
|
|
3
|
+
import {CallToolResultSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import {
|
|
5
|
+
catalogTool,
|
|
6
|
+
connection,
|
|
7
|
+
materializedIntegration,
|
|
8
|
+
materializedTool,
|
|
9
|
+
} from '#test/agent-tools-gateway-helpers.js';
|
|
10
|
+
import {INVALID_METHOD_LABEL, NO_METHOD_LABEL} from './audit.js';
|
|
11
|
+
import {buildAgentToolsMcpServer, type IntegrationToolDispatchInput} from './mcp-server.js';
|
|
12
|
+
import type {AuthorizedIntegrationToolMap} from './resolve-authorized-tools.js';
|
|
13
|
+
|
|
14
|
+
describe('buildAgentToolsMcpServer', () => {
|
|
15
|
+
it('lists namespaced tools and dispatches authorized method-family calls', async () => {
|
|
16
|
+
const dispatch = vi.fn(async (input: IntegrationToolDispatchInput) => ({
|
|
17
|
+
content: [{type: 'text' as const, text: `called:${input.method}`}],
|
|
18
|
+
}));
|
|
19
|
+
const records: Parameters<
|
|
20
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
21
|
+
>[0][] = [];
|
|
22
|
+
const {client, close} = await connectClient(dispatch, defaultAuthorizedTools(), (record) =>
|
|
23
|
+
records.push(record),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const tools = await client.listTools();
|
|
27
|
+
const result = await client.callTool(
|
|
28
|
+
{
|
|
29
|
+
name: 'github_main__issue_read',
|
|
30
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
31
|
+
},
|
|
32
|
+
CallToolResultSchema,
|
|
33
|
+
);
|
|
34
|
+
await close();
|
|
35
|
+
|
|
36
|
+
expect(tools.tools).toMatchObject([
|
|
37
|
+
{
|
|
38
|
+
name: 'github_main__issue_read',
|
|
39
|
+
description: 'Read issue metadata from GitHub.',
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
expect(result.isError).not.toBe(true);
|
|
43
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
method: 'get',
|
|
46
|
+
arguments: expect.objectContaining({issue_number: 1}),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
expect(records).toMatchObject([
|
|
50
|
+
{
|
|
51
|
+
authorizedTool: expect.objectContaining({
|
|
52
|
+
integration: expect.objectContaining({provider: 'github'}),
|
|
53
|
+
tool: expect.objectContaining({id: 'issue_read'}),
|
|
54
|
+
}),
|
|
55
|
+
method: 'get',
|
|
56
|
+
outcome: 'success',
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([
|
|
62
|
+
['unknown tool', 'missing__tool', {method: 'get'}, NO_METHOD_LABEL, undefined],
|
|
63
|
+
['missing method', 'github_main__issue_read', {}, INVALID_METHOD_LABEL, 'issue_read'],
|
|
64
|
+
[
|
|
65
|
+
'non-string method',
|
|
66
|
+
'github_main__issue_read',
|
|
67
|
+
{method: 1},
|
|
68
|
+
INVALID_METHOD_LABEL,
|
|
69
|
+
'issue_read',
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
'unauthorized method',
|
|
73
|
+
'github_main__issue_read',
|
|
74
|
+
{method: 'get_labels'},
|
|
75
|
+
INVALID_METHOD_LABEL,
|
|
76
|
+
'issue_read',
|
|
77
|
+
],
|
|
78
|
+
])('returns an isError tool result and records invalid-request for %s', async (_label, name, args, expectedMethod, expectedToolId) => {
|
|
79
|
+
const dispatch = vi.fn();
|
|
80
|
+
const records: Parameters<
|
|
81
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
82
|
+
>[0][] = [];
|
|
83
|
+
const {client, close} = await connectClient(dispatch, defaultAuthorizedTools(), (record) =>
|
|
84
|
+
records.push(record),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const result = await client.callTool({name, arguments: args}, CallToolResultSchema);
|
|
88
|
+
await close();
|
|
89
|
+
|
|
90
|
+
expect(result.isError).toBe(true);
|
|
91
|
+
expect(dispatch).not.toHaveBeenCalled();
|
|
92
|
+
expect(records).toMatchObject([
|
|
93
|
+
{
|
|
94
|
+
method: expectedMethod,
|
|
95
|
+
outcome: 'invalid-request',
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
if (expectedToolId) {
|
|
99
|
+
expect(records[0]?.authorizedTool).toEqual(
|
|
100
|
+
expect.objectContaining({tool: expect.objectContaining({id: expectedToolId})}),
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
expect(records[0]?.authorizedTool).toBeUndefined();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('ignores a stray method argument for standalone tools', async () => {
|
|
108
|
+
const dispatch = vi.fn(async () => ({
|
|
109
|
+
content: [{type: 'text' as const, text: 'called'}],
|
|
110
|
+
}));
|
|
111
|
+
const records: Parameters<
|
|
112
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
113
|
+
>[0][] = [];
|
|
114
|
+
const {client, close} = await connectClient(dispatch, standaloneTools(), (record) =>
|
|
115
|
+
records.push(record),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const result = await client.callTool(
|
|
119
|
+
{name: 'github_main__list_issues', arguments: {method: 'ignored'}},
|
|
120
|
+
CallToolResultSchema,
|
|
121
|
+
);
|
|
122
|
+
await close();
|
|
123
|
+
|
|
124
|
+
expect(result.isError).not.toBe(true);
|
|
125
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
method: undefined,
|
|
128
|
+
arguments: {method: 'ignored'},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
expect(records).toMatchObject([{method: NO_METHOD_LABEL, outcome: 'success'}]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('defaults omitted arguments to an empty object for optional-argument tools', async () => {
|
|
135
|
+
const dispatch = vi.fn(async () => ({
|
|
136
|
+
content: [{type: 'text' as const, text: 'called'}],
|
|
137
|
+
}));
|
|
138
|
+
const {client, close} = await connectClient(dispatch, standaloneTools());
|
|
139
|
+
|
|
140
|
+
const result = await client.callTool({name: 'github_main__list_issues'}, CallToolResultSchema);
|
|
141
|
+
await close();
|
|
142
|
+
|
|
143
|
+
expect(result.isError).not.toBe(true);
|
|
144
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
arguments: {},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('leaves provider argument validation to the selected provider', async () => {
|
|
152
|
+
const dispatch = vi.fn(async () => ({
|
|
153
|
+
content: [{type: 'text' as const, text: 'called'}],
|
|
154
|
+
}));
|
|
155
|
+
const records: Parameters<
|
|
156
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
157
|
+
>[0][] = [];
|
|
158
|
+
const {client, close} = await connectClient(dispatch, defaultAuthorizedTools(), (record) =>
|
|
159
|
+
records.push(record),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const result = await client.callTool(
|
|
163
|
+
{
|
|
164
|
+
name: 'github_main__issue_read',
|
|
165
|
+
arguments: {
|
|
166
|
+
method: 'get',
|
|
167
|
+
owner: 'shipfox',
|
|
168
|
+
repo: 'platform',
|
|
169
|
+
issue_number: 'not-an-integer',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
CallToolResultSchema,
|
|
173
|
+
);
|
|
174
|
+
await close();
|
|
175
|
+
|
|
176
|
+
expect(result.isError).not.toBe(true);
|
|
177
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
arguments: expect.objectContaining({issue_number: 'not-an-integer'}),
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
expect(records).toMatchObject([{method: 'get', outcome: 'success'}]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('records tool-error when dispatch returns an error result', async () => {
|
|
186
|
+
const dispatch = vi.fn(async () => ({
|
|
187
|
+
isError: true,
|
|
188
|
+
content: [{type: 'text' as const, text: 'provider rejected call'}],
|
|
189
|
+
}));
|
|
190
|
+
const records: Parameters<
|
|
191
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
192
|
+
>[0][] = [];
|
|
193
|
+
const {client, close} = await connectClient(dispatch, defaultAuthorizedTools(), (record) =>
|
|
194
|
+
records.push(record),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const result = await client.callTool(
|
|
198
|
+
{
|
|
199
|
+
name: 'github_main__issue_read',
|
|
200
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
201
|
+
},
|
|
202
|
+
CallToolResultSchema,
|
|
203
|
+
);
|
|
204
|
+
await close();
|
|
205
|
+
|
|
206
|
+
expect(result.isError).toBe(true);
|
|
207
|
+
expect(records).toMatchObject([{method: 'get', outcome: 'tool-error'}]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('records exception before rethrowing dispatcher failures', async () => {
|
|
211
|
+
const dispatch = vi.fn(() => Promise.reject(new Error('provider unavailable')));
|
|
212
|
+
const records: Parameters<
|
|
213
|
+
NonNullable<Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall']>
|
|
214
|
+
>[0][] = [];
|
|
215
|
+
const {client, close} = await connectClient(dispatch, defaultAuthorizedTools(), (record) =>
|
|
216
|
+
records.push(record),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await expect(
|
|
220
|
+
client.callTool(
|
|
221
|
+
{
|
|
222
|
+
name: 'github_main__issue_read',
|
|
223
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
224
|
+
},
|
|
225
|
+
CallToolResultSchema,
|
|
226
|
+
),
|
|
227
|
+
).rejects.toThrow('MCP error -32603');
|
|
228
|
+
await close();
|
|
229
|
+
|
|
230
|
+
expect(records).toMatchObject([{method: 'get', outcome: 'exception'}]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
async function connectClient(
|
|
235
|
+
dispatch: Parameters<typeof buildAgentToolsMcpServer>[0]['dispatch'],
|
|
236
|
+
authorizedTools = defaultAuthorizedTools(),
|
|
237
|
+
recordCall?: Parameters<typeof buildAgentToolsMcpServer>[0]['recordCall'],
|
|
238
|
+
) {
|
|
239
|
+
const server = buildAgentToolsMcpServer({authorizedTools, dispatch, recordCall});
|
|
240
|
+
const client = new Client({name: 'test-client', version: '0.0.0'});
|
|
241
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
242
|
+
|
|
243
|
+
await server.connect(serverTransport);
|
|
244
|
+
await client.connect(clientTransport);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
client,
|
|
248
|
+
close: async () => {
|
|
249
|
+
await client.close();
|
|
250
|
+
await server.close();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function defaultAuthorizedTools(): AuthorizedIntegrationToolMap {
|
|
256
|
+
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
257
|
+
const tool = materializedTool();
|
|
258
|
+
return new Map([
|
|
259
|
+
[
|
|
260
|
+
'github_main__issue_read',
|
|
261
|
+
{
|
|
262
|
+
mcpName: 'github_main__issue_read',
|
|
263
|
+
integration,
|
|
264
|
+
tool,
|
|
265
|
+
connection: connection({
|
|
266
|
+
id: 'connection-1',
|
|
267
|
+
workspaceId: 'workspace-1',
|
|
268
|
+
slug: integration.connectionSlug,
|
|
269
|
+
}),
|
|
270
|
+
description: catalogTool().description,
|
|
271
|
+
inputSchema: tool.inputSchema,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function standaloneTools(): AuthorizedIntegrationToolMap {
|
|
278
|
+
const integration = materializedIntegration({
|
|
279
|
+
connectionId: 'connection-1',
|
|
280
|
+
tools: [
|
|
281
|
+
materializedTool({
|
|
282
|
+
id: 'list_issues',
|
|
283
|
+
methods: undefined,
|
|
284
|
+
inputSchema: {type: 'object', properties: {}, additionalProperties: true},
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
const [tool] = integration.tools;
|
|
289
|
+
if (!tool) throw new Error('Expected standalone integration tool');
|
|
290
|
+
|
|
291
|
+
return new Map([
|
|
292
|
+
[
|
|
293
|
+
'github_main__list_issues',
|
|
294
|
+
{
|
|
295
|
+
mcpName: 'github_main__list_issues',
|
|
296
|
+
integration,
|
|
297
|
+
tool,
|
|
298
|
+
connection: connection({
|
|
299
|
+
id: 'connection-1',
|
|
300
|
+
workspaceId: 'workspace-1',
|
|
301
|
+
slug: integration.connectionSlug,
|
|
302
|
+
}),
|
|
303
|
+
description: 'List issues',
|
|
304
|
+
inputSchema: tool.inputSchema,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
type CallToolResult,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import {INVALID_METHOD_LABEL, type IntegrationToolCallRecorder, NO_METHOD_LABEL} from './audit.js';
|
|
8
|
+
import type {
|
|
9
|
+
AuthorizedIntegrationTool,
|
|
10
|
+
AuthorizedIntegrationToolMap,
|
|
11
|
+
} from './resolve-authorized-tools.js';
|
|
12
|
+
|
|
13
|
+
export interface IntegrationToolDispatchInput {
|
|
14
|
+
authorizedTool: AuthorizedIntegrationTool;
|
|
15
|
+
arguments: Record<string, unknown>;
|
|
16
|
+
method?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type IntegrationToolDispatcher = (
|
|
20
|
+
input: IntegrationToolDispatchInput,
|
|
21
|
+
) => Promise<CallToolResult>;
|
|
22
|
+
|
|
23
|
+
export interface BuildAgentToolsMcpServerParams {
|
|
24
|
+
authorizedTools: AuthorizedIntegrationToolMap;
|
|
25
|
+
dispatch: IntegrationToolDispatcher;
|
|
26
|
+
recordCall?: IntegrationToolCallRecorder | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildAgentToolsMcpServer(params: BuildAgentToolsMcpServerParams): Server {
|
|
30
|
+
const server = new Server(
|
|
31
|
+
{name: 'shipfox-integration-tools', version: '0.0.0'},
|
|
32
|
+
{capabilities: {tools: {}}},
|
|
33
|
+
);
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
35
|
+
tools: [...params.authorizedTools.values()].map((authorizedTool) => ({
|
|
36
|
+
name: authorizedTool.mcpName,
|
|
37
|
+
description: authorizedTool.description,
|
|
38
|
+
inputSchema: authorizedTool.inputSchema as {
|
|
39
|
+
type: 'object';
|
|
40
|
+
properties?: Record<string, object> | undefined;
|
|
41
|
+
required?: string[] | undefined;
|
|
42
|
+
},
|
|
43
|
+
...(authorizedTool.outputSchema
|
|
44
|
+
? {
|
|
45
|
+
outputSchema: authorizedTool.outputSchema as {
|
|
46
|
+
type: 'object';
|
|
47
|
+
properties?: Record<string, object> | undefined;
|
|
48
|
+
required?: string[] | undefined;
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
})),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
56
|
+
const authorizedTool = params.authorizedTools.get(request.params.name);
|
|
57
|
+
if (!authorizedTool) {
|
|
58
|
+
recordToolCall(params.recordCall, {
|
|
59
|
+
arguments: request.params.arguments ?? {},
|
|
60
|
+
method: NO_METHOD_LABEL,
|
|
61
|
+
outcome: 'invalid-request',
|
|
62
|
+
});
|
|
63
|
+
return toolError(`Unknown integration tool: ${request.params.name}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const args = request.params.arguments ?? {};
|
|
67
|
+
if (!isRecord(args)) {
|
|
68
|
+
recordToolCall(params.recordCall, {
|
|
69
|
+
authorizedTool,
|
|
70
|
+
arguments: args,
|
|
71
|
+
method: NO_METHOD_LABEL,
|
|
72
|
+
outcome: 'invalid-request',
|
|
73
|
+
});
|
|
74
|
+
return toolError('Tool arguments must be an object');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const methodValidation = validateMethod(authorizedTool, args);
|
|
78
|
+
if (methodValidation.kind === 'error') {
|
|
79
|
+
recordToolCall(params.recordCall, {
|
|
80
|
+
authorizedTool,
|
|
81
|
+
arguments: args,
|
|
82
|
+
method: INVALID_METHOD_LABEL,
|
|
83
|
+
outcome: 'invalid-request',
|
|
84
|
+
});
|
|
85
|
+
return toolError(methodValidation.message);
|
|
86
|
+
}
|
|
87
|
+
const method = methodValidation.method ?? NO_METHOD_LABEL;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await params.dispatch({
|
|
91
|
+
authorizedTool,
|
|
92
|
+
arguments: args,
|
|
93
|
+
method: methodValidation.method,
|
|
94
|
+
});
|
|
95
|
+
recordToolCall(params.recordCall, {
|
|
96
|
+
authorizedTool,
|
|
97
|
+
arguments: args,
|
|
98
|
+
method,
|
|
99
|
+
outcome: result.isError === true ? 'tool-error' : 'success',
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
recordToolCall(params.recordCall, {
|
|
104
|
+
authorizedTool,
|
|
105
|
+
arguments: args,
|
|
106
|
+
method,
|
|
107
|
+
outcome: 'exception',
|
|
108
|
+
});
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return server;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function recordToolCall(
|
|
117
|
+
recordCall: IntegrationToolCallRecorder | undefined,
|
|
118
|
+
record: Parameters<IntegrationToolCallRecorder>[0],
|
|
119
|
+
): void {
|
|
120
|
+
try {
|
|
121
|
+
recordCall?.(record);
|
|
122
|
+
} catch {
|
|
123
|
+
// Audit and metrics must not affect MCP responses.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateMethod(
|
|
128
|
+
authorizedTool: AuthorizedIntegrationTool,
|
|
129
|
+
args: Record<string, unknown>,
|
|
130
|
+
): {kind: 'ok'; method?: string | undefined} | {kind: 'error'; message: string} {
|
|
131
|
+
if (!authorizedTool.tool.methods) return {kind: 'ok'};
|
|
132
|
+
|
|
133
|
+
const method = args.method;
|
|
134
|
+
if (typeof method !== 'string') {
|
|
135
|
+
return {kind: 'error', message: 'Method-family tools require a string method argument'};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const allowedMethods = new Set(authorizedTool.tool.methods.map((candidate) => candidate.id));
|
|
139
|
+
if (!allowedMethods.has(method)) {
|
|
140
|
+
return {kind: 'error', message: `Unauthorized integration tool method: ${method}`};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {kind: 'ok', method};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toolError(message: string): CallToolResult {
|
|
147
|
+
return {
|
|
148
|
+
isError: true,
|
|
149
|
+
content: [{type: 'text', text: message}],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
154
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
155
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {setLeasedJobContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {createIntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
3
|
+
import {
|
|
4
|
+
agentStepConfig,
|
|
5
|
+
catalogTool,
|
|
6
|
+
connection,
|
|
7
|
+
leaseContext,
|
|
8
|
+
materializedIntegration,
|
|
9
|
+
registryWithAgentTools,
|
|
10
|
+
} from '#test/agent-tools-gateway-helpers.js';
|
|
11
|
+
import {
|
|
12
|
+
mcpToolName,
|
|
13
|
+
narrowMethodEnum,
|
|
14
|
+
resolveAuthorizedIntegrationTools,
|
|
15
|
+
sanitizeSlug,
|
|
16
|
+
} from './resolve-authorized-tools.js';
|
|
17
|
+
|
|
18
|
+
describe('resolveAuthorizedIntegrationTools', () => {
|
|
19
|
+
it('resolves namespaced tools with live descriptions and Anthropic-compatible method schemas', async () => {
|
|
20
|
+
const request = {};
|
|
21
|
+
const lease = leaseContext();
|
|
22
|
+
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
23
|
+
setLeasedJobContext(request, lease);
|
|
24
|
+
|
|
25
|
+
const result = await resolveAuthorizedIntegrationTools({
|
|
26
|
+
request,
|
|
27
|
+
registry: registryWithAgentTools([catalogTool({description: 'Live issue reader'})]),
|
|
28
|
+
getIntegrationConnectionById: async () =>
|
|
29
|
+
connection({
|
|
30
|
+
id: 'connection-1',
|
|
31
|
+
workspaceId: lease.workspaceId,
|
|
32
|
+
slug: integration.connectionSlug,
|
|
33
|
+
}),
|
|
34
|
+
loadLeasedAgentStep: async () => ({
|
|
35
|
+
workspaceId: lease.workspaceId,
|
|
36
|
+
step: {type: 'agent', config: agentStepConfig([integration])},
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const authorizedTool = result.get('github_main__issue_read');
|
|
41
|
+
expect(authorizedTool?.description).toBe('Live issue reader');
|
|
42
|
+
const properties = authorizedTool?.inputSchema.properties as Record<string, unknown>;
|
|
43
|
+
expect(properties.method).toMatchObject({
|
|
44
|
+
enum: ['get', 'get_comments'],
|
|
45
|
+
});
|
|
46
|
+
expect(authorizedTool?.inputSchema.oneOf).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('fails closed when the lease has no current step', async () => {
|
|
50
|
+
const request = {};
|
|
51
|
+
setLeasedJobContext(
|
|
52
|
+
request,
|
|
53
|
+
leaseContext({currentStepId: undefined, currentStepAttempt: undefined}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const act = resolveAuthorizedIntegrationTools({
|
|
57
|
+
request,
|
|
58
|
+
registry: registryWithAgentTools(),
|
|
59
|
+
getIntegrationConnectionById: async () => undefined,
|
|
60
|
+
loadLeasedAgentStep: () => Promise.reject(new Error('should not load')),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await expect(act).rejects.toMatchObject({code: 'lease-missing-step', status: 409});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('fails closed when the current step is not an agent step', async () => {
|
|
67
|
+
const request = {};
|
|
68
|
+
const lease = leaseContext();
|
|
69
|
+
setLeasedJobContext(request, lease);
|
|
70
|
+
|
|
71
|
+
const act = resolveAuthorizedIntegrationTools({
|
|
72
|
+
request,
|
|
73
|
+
registry: registryWithAgentTools(),
|
|
74
|
+
getIntegrationConnectionById: async () => undefined,
|
|
75
|
+
loadLeasedAgentStep: async () => ({
|
|
76
|
+
workspaceId: lease.workspaceId,
|
|
77
|
+
step: {type: 'run', config: {}},
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await expect(act).rejects.toMatchObject({code: 'leased-step-not-agent', status: 409});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
['deleted', undefined],
|
|
86
|
+
['inactive', connection({id: 'connection-1', lifecycleStatus: 'disabled'})],
|
|
87
|
+
['workspace mismatch', connection({id: 'connection-1', workspaceId: 'other-workspace'})],
|
|
88
|
+
])('denies when the connection is %s', async (_label, resolvedConnection) => {
|
|
89
|
+
const request = {};
|
|
90
|
+
const lease = leaseContext({workspaceId: 'workspace-1'});
|
|
91
|
+
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
92
|
+
setLeasedJobContext(request, lease);
|
|
93
|
+
|
|
94
|
+
const act = resolveAuthorizedIntegrationTools({
|
|
95
|
+
request,
|
|
96
|
+
registry: registryWithAgentTools(),
|
|
97
|
+
getIntegrationConnectionById: async () => resolvedConnection,
|
|
98
|
+
loadLeasedAgentStep: async () => ({
|
|
99
|
+
workspaceId: lease.workspaceId,
|
|
100
|
+
step: {type: 'agent', config: agentStepConfig([integration])},
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await expect(act).rejects.toMatchObject({
|
|
105
|
+
code: 'integration-tool-connection-unavailable',
|
|
106
|
+
status: 409,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('denies when the provider no longer exposes agent tools', async () => {
|
|
111
|
+
const request = {};
|
|
112
|
+
const lease = leaseContext({workspaceId: 'workspace-1'});
|
|
113
|
+
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
114
|
+
setLeasedJobContext(request, lease);
|
|
115
|
+
|
|
116
|
+
const act = resolveAuthorizedIntegrationTools({
|
|
117
|
+
request,
|
|
118
|
+
registry: createIntegrationProviderRegistry([{provider: 'github', displayName: 'GitHub'}]),
|
|
119
|
+
getIntegrationConnectionById: async () =>
|
|
120
|
+
connection({id: 'connection-1', workspaceId: lease.workspaceId}),
|
|
121
|
+
loadLeasedAgentStep: async () => ({
|
|
122
|
+
workspaceId: lease.workspaceId,
|
|
123
|
+
step: {type: 'agent', config: agentStepConfig([integration])},
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await expect(act).rejects.toMatchObject({
|
|
128
|
+
code: 'integration-tool-connection-unavailable',
|
|
129
|
+
status: 409,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('detects MCP name collisions after slug sanitization', async () => {
|
|
134
|
+
const request = {};
|
|
135
|
+
const lease = leaseContext({workspaceId: 'workspace-1'});
|
|
136
|
+
const first = materializedIntegration({
|
|
137
|
+
connectionId: 'connection-1',
|
|
138
|
+
connectionSlug: 'github-main',
|
|
139
|
+
});
|
|
140
|
+
const second = materializedIntegration({
|
|
141
|
+
connectionId: 'connection-2',
|
|
142
|
+
connectionSlug: 'github_main',
|
|
143
|
+
});
|
|
144
|
+
setLeasedJobContext(request, lease);
|
|
145
|
+
|
|
146
|
+
const act = resolveAuthorizedIntegrationTools({
|
|
147
|
+
request,
|
|
148
|
+
registry: registryWithAgentTools(),
|
|
149
|
+
getIntegrationConnectionById: async (id) =>
|
|
150
|
+
connection({
|
|
151
|
+
id,
|
|
152
|
+
workspaceId: lease.workspaceId,
|
|
153
|
+
slug: id === 'connection-1' ? 'github-main' : 'github_main',
|
|
154
|
+
}),
|
|
155
|
+
loadLeasedAgentStep: async () => ({
|
|
156
|
+
workspaceId: lease.workspaceId,
|
|
157
|
+
step: {type: 'agent', config: agentStepConfig([first, second])},
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await expect(act).rejects.toMatchObject({
|
|
162
|
+
code: 'integration-tool-name-collision',
|
|
163
|
+
status: 409,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('leaves unknown method schema shapes intact while runtime enforcement remains separate', () => {
|
|
168
|
+
const schema = {type: 'object', properties: {method: {type: 'string'}}};
|
|
169
|
+
|
|
170
|
+
const narrowed = narrowMethodEnum(schema, ['allowed']);
|
|
171
|
+
|
|
172
|
+
expect(narrowed).toEqual(schema);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('MCP tool names', () => {
|
|
177
|
+
it('sanitizes connection slugs without parsing names back apart', () => {
|
|
178
|
+
expect(sanitizeSlug('github-main')).toBe('github_main');
|
|
179
|
+
expect(mcpToolName('github-main', 'issue_read')).toBe('github_main__issue_read');
|
|
180
|
+
});
|
|
181
|
+
});
|