aui-agent-builder 0.4.6 → 0.4.8
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/dist/api-client/apollo-client.d.ts +102 -5
- package/dist/api-client/apollo-client.d.ts.map +1 -1
- package/dist/api-client/apollo-client.js +136 -5
- package/dist/api-client/apollo-client.js.map +1 -1
- package/dist/api-client/mock-db-client.d.ts +11 -0
- package/dist/api-client/mock-db-client.d.ts.map +1 -1
- package/dist/api-client/mock-db-client.js +15 -0
- package/dist/api-client/mock-db-client.js.map +1 -1
- package/dist/commands/import-agent.d.ts +2 -5
- package/dist/commands/import-agent.d.ts.map +1 -1
- package/dist/commands/import-agent.js +1 -1
- package/dist/commands/import-agent.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/integration-mcp-test.d.ts +0 -2
- package/dist/commands/integration-mcp-test.d.ts.map +1 -1
- package/dist/commands/integration-mcp-test.js +41 -135
- package/dist/commands/integration-mcp-test.js.map +1 -1
- package/dist/commands/integration-mcp-url.d.ts +6 -7
- package/dist/commands/integration-mcp-url.d.ts.map +1 -1
- package/dist/commands/integration-mcp-url.js +10 -29
- package/dist/commands/integration-mcp-url.js.map +1 -1
- package/dist/commands/integration-toolkits.d.ts +4 -11
- package/dist/commands/integration-toolkits.d.ts.map +1 -1
- package/dist/commands/integration-toolkits.js +6 -28
- package/dist/commands/integration-toolkits.js.map +1 -1
- package/dist/commands/integration-tools.d.ts +5 -15
- package/dist/commands/integration-tools.d.ts.map +1 -1
- package/dist/commands/integration-tools.js +17 -63
- package/dist/commands/integration-tools.js.map +1 -1
- package/dist/commands/integration.d.ts +0 -1
- package/dist/commands/integration.d.ts.map +1 -1
- package/dist/commands/integration.js +62 -168
- package/dist/commands/integration.js.map +1 -1
- package/dist/commands/mockdb-guide.d.ts +1 -1
- package/dist/commands/mockdb-guide.d.ts.map +1 -1
- package/dist/commands/mockdb-guide.js +18 -0
- package/dist/commands/mockdb-guide.js.map +1 -1
- package/dist/commands/pull-agent.d.ts +2 -5
- package/dist/commands/pull-agent.d.ts.map +1 -1
- package/dist/commands/pull-agent.js +1 -1
- package/dist/commands/pull-agent.js.map +1 -1
- package/dist/config/index.d.ts +36 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -5
- package/dist/config/index.js.map +1 -1
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +20 -9
- package/dist/errors/index.js.map +1 -1
- package/dist/index.js +43 -34
- package/dist/index.js.map +1 -1
- package/dist/services/integration.service.d.ts +73 -148
- package/dist/services/integration.service.d.ts.map +1 -1
- package/dist/services/integration.service.js +400 -559
- package/dist/services/integration.service.js.map +1 -1
- package/dist/services/mock-db.service.d.ts +13 -1
- package/dist/services/mock-db.service.d.ts.map +1 -1
- package/dist/services/mock-db.service.js +98 -2
- package/dist/services/mock-db.service.js.map +1 -1
- package/dist/services/pull-schema.service.d.ts +4 -5
- package/dist/services/pull-schema.service.d.ts.map +1 -1
- package/dist/services/pull-schema.service.js +12 -10
- package/dist/services/pull-schema.service.js.map +1 -1
- package/dist/ui/components/ErrorDisplay.d.ts.map +1 -1
- package/dist/ui/components/ErrorDisplay.js +16 -4
- package/dist/ui/components/ErrorDisplay.js.map +1 -1
- package/package.json +1 -2
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
2
4
|
import { randomUUID } from "crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { getConfig, loadProjectConfig, getBaseUrl, getAgentSettingsWriteUrl, findProjectRoot, } from "../config/index.js";
|
|
6
|
+
import { AUIAPIError, AUIClient } from "../api-client/index.js";
|
|
7
|
+
import { detectAgentBundleMode, isModeMismatchError, } from "../commands/util/agent-mode.js";
|
|
8
|
+
import { parseAuiFile, writeJsonFile } from "../utils/index.js";
|
|
6
9
|
import { ApolloClient, } from "../api-client/apollo-client.js";
|
|
7
|
-
import { AuthenticationError, ValidationError } from "../errors/index.js";
|
|
10
|
+
import { AuthenticationError, CLIError, ValidationError, toCLIError, } from "../errors/index.js";
|
|
8
11
|
import { getValidSession } from "./auth.service.js";
|
|
9
12
|
import { captureRequest } from "../utils/request-capture.js";
|
|
10
13
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -109,6 +112,31 @@ export function buildAuthFromFlags(opts) {
|
|
|
109
112
|
...(opts.authHeaderName ? { header_name: opts.authHeaderName } : {}),
|
|
110
113
|
};
|
|
111
114
|
}
|
|
115
|
+
/** Load BYO Composio OAuth credentials from a JSON file (`client_id`, etc.). */
|
|
116
|
+
export function loadComposioCredentialsFile(filePath) {
|
|
117
|
+
let raw;
|
|
118
|
+
try {
|
|
119
|
+
raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
throw new Error(`Cannot read credentials file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
125
|
+
throw new Error(`Credentials file '${filePath}' must be a JSON object.`);
|
|
126
|
+
}
|
|
127
|
+
const obj = raw;
|
|
128
|
+
const creds = {};
|
|
129
|
+
if (obj.client_id)
|
|
130
|
+
creds.clientId = String(obj.client_id);
|
|
131
|
+
if (obj.client_secret)
|
|
132
|
+
creds.clientSecret = String(obj.client_secret);
|
|
133
|
+
if (obj.bearer_token)
|
|
134
|
+
creds.bearerToken = String(obj.bearer_token);
|
|
135
|
+
if (!creds.clientId && !creds.clientSecret && !creds.bearerToken) {
|
|
136
|
+
throw new Error(`Credentials file '${filePath}' must contain at least one of: client_id, client_secret, bearer_token.`);
|
|
137
|
+
}
|
|
138
|
+
return creds;
|
|
139
|
+
}
|
|
112
140
|
// ─── Session ───
|
|
113
141
|
export async function getAuthenticatedSession() {
|
|
114
142
|
const config = getConfig();
|
|
@@ -123,6 +151,79 @@ export async function getAuthenticatedSession() {
|
|
|
123
151
|
}
|
|
124
152
|
return session;
|
|
125
153
|
}
|
|
154
|
+
const COMPOSIO_AUTH_BODY_PATTERNS = [
|
|
155
|
+
/unauthorized/i,
|
|
156
|
+
/unauthenticated/i,
|
|
157
|
+
/not authenticated/i,
|
|
158
|
+
/authentication required/i,
|
|
159
|
+
/invalid.*token/i,
|
|
160
|
+
/session.*expired/i,
|
|
161
|
+
/auth.*required/i,
|
|
162
|
+
];
|
|
163
|
+
function composioAuthDetail(body) {
|
|
164
|
+
if (body == null)
|
|
165
|
+
return undefined;
|
|
166
|
+
if (typeof body === "string")
|
|
167
|
+
return body.trim() || undefined;
|
|
168
|
+
if (typeof body === "object" && "detail" in body) {
|
|
169
|
+
const detail = body.detail;
|
|
170
|
+
if (typeof detail === "string")
|
|
171
|
+
return detail.trim() || undefined;
|
|
172
|
+
if (Array.isArray(detail)) {
|
|
173
|
+
return detail
|
|
174
|
+
.map((item) => typeof item === "object" && item !== null && "msg" in item
|
|
175
|
+
? String(item.msg)
|
|
176
|
+
: String(item))
|
|
177
|
+
.join("; ");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const text = JSON.stringify(body);
|
|
181
|
+
return COMPOSIO_AUTH_BODY_PATTERNS.some((re) => re.test(text))
|
|
182
|
+
? text
|
|
183
|
+
: undefined;
|
|
184
|
+
}
|
|
185
|
+
function bodyLooksLikeAuthFailure(body) {
|
|
186
|
+
const detail = composioAuthDetail(body);
|
|
187
|
+
return detail != null && COMPOSIO_AUTH_BODY_PATTERNS.some((re) => re.test(detail));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Turn a Composio/Apollo MCP API failure into an actionable CLI error.
|
|
191
|
+
* Auth failures surface as AuthenticationError with login guidance instead
|
|
192
|
+
* of a generic "failed to fetch" message.
|
|
193
|
+
*/
|
|
194
|
+
export function composioApiError(error) {
|
|
195
|
+
if (error instanceof AUIAPIError && bodyLooksLikeAuthFailure(error.body)) {
|
|
196
|
+
return new AuthenticationError(composioAuthDetail(error.body) ?? `Authentication failed (${error.status}).`, { cause: error });
|
|
197
|
+
}
|
|
198
|
+
const classified = toCLIError(error);
|
|
199
|
+
if (classified instanceof AuthenticationError) {
|
|
200
|
+
return classified;
|
|
201
|
+
}
|
|
202
|
+
if (classified.code !== "UNKNOWN_ERROR") {
|
|
203
|
+
return classified;
|
|
204
|
+
}
|
|
205
|
+
return new CLIError(classified.message, {
|
|
206
|
+
code: classified.code,
|
|
207
|
+
suggestion: classified.suggestion ??
|
|
208
|
+
"Verify your `aui login` session is still valid and that Composio is configured on the backend.",
|
|
209
|
+
cause: error,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Short spinner label for a Composio API failure. */
|
|
213
|
+
export function composioFailureLabel(error, defaultLabel) {
|
|
214
|
+
return composioApiError(error) instanceof AuthenticationError
|
|
215
|
+
? "Not authenticated"
|
|
216
|
+
: defaultLabel;
|
|
217
|
+
}
|
|
218
|
+
/** JSON envelope fields for a Composio API failure. */
|
|
219
|
+
export function composioFailureJson(error) {
|
|
220
|
+
const cliErr = composioApiError(error);
|
|
221
|
+
return {
|
|
222
|
+
code: cliErr.code,
|
|
223
|
+
message: cliErr.message,
|
|
224
|
+
...(cliErr.suggestion ? { suggestion: cliErr.suggestion } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
126
227
|
// ─── Scope Resolution ───
|
|
127
228
|
export function resolveScopeIds(session, overrides) {
|
|
128
229
|
const projectConfig = loadProjectConfig();
|
|
@@ -167,13 +268,7 @@ export function assertComposioUserIdMatchesNetworkId(composioUserId, networkId)
|
|
|
167
268
|
export async function resolveNetworkCategoryId(session, networkId, scope) {
|
|
168
269
|
if (scope.networkCategoryId)
|
|
169
270
|
return scope.networkCategoryId;
|
|
170
|
-
const client =
|
|
171
|
-
baseUrl: session.api_url || getBaseUrl(session.environment),
|
|
172
|
-
authToken: session.auth_token,
|
|
173
|
-
accountId: scope.accountId,
|
|
174
|
-
organizationId: scope.organizationId,
|
|
175
|
-
environment: session.environment,
|
|
176
|
-
});
|
|
271
|
+
const client = buildAUIClient(session, scope);
|
|
177
272
|
try {
|
|
178
273
|
const resp = await client.networks.get(networkId);
|
|
179
274
|
const network = resp.data;
|
|
@@ -320,6 +415,101 @@ function extractTools(parsed) {
|
|
|
320
415
|
return [];
|
|
321
416
|
}
|
|
322
417
|
// ─── Save ───
|
|
418
|
+
const BUNDLE_MODE_LOCAL_SUGGESTION = "Integration saved to integrations.aui.json. Run `aui push` to upload it to the server.";
|
|
419
|
+
const NO_PROJECT_ERROR = "Run this command inside an imported agent project (with .auirc).";
|
|
420
|
+
const NO_PROJECT_SUGGESTION = "Run `aui import` or `cd` into your agent directory, then re-run `aui integration create`.";
|
|
421
|
+
function integrationCodeFromName(name) {
|
|
422
|
+
return name
|
|
423
|
+
.toLowerCase()
|
|
424
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
425
|
+
.replace(/^-|-$/g, "");
|
|
426
|
+
}
|
|
427
|
+
function buildLocalMCPIntegrationRecord(request, code) {
|
|
428
|
+
return {
|
|
429
|
+
type: "MCP",
|
|
430
|
+
code,
|
|
431
|
+
name: request.display_name,
|
|
432
|
+
description: request.description,
|
|
433
|
+
settings: buildMCPSettings(request),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function mergeIntegrationIntoLocalFile(projectRoot, integration) {
|
|
437
|
+
const filePath = path.join(projectRoot, "integrations.aui.json");
|
|
438
|
+
let integrations = [];
|
|
439
|
+
if (existsSync(filePath)) {
|
|
440
|
+
const parsed = parseAuiFile(filePath);
|
|
441
|
+
const existing = parsed?.integrations;
|
|
442
|
+
if (Array.isArray(existing)) {
|
|
443
|
+
integrations = existing.filter((item) => !!item && typeof item === "object" && !Array.isArray(item));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const code = String(integration.code);
|
|
447
|
+
const idx = integrations.findIndex((item) => String(item.code) === code);
|
|
448
|
+
if (idx >= 0) {
|
|
449
|
+
integrations[idx] = integration;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
integrations.push(integration);
|
|
453
|
+
}
|
|
454
|
+
writeJsonFile(filePath, { integrations });
|
|
455
|
+
return filePath;
|
|
456
|
+
}
|
|
457
|
+
/** Agent-settings / agent-management client — same session+scope wiring as {@link buildApolloClient}. */
|
|
458
|
+
export function buildAUIClient(session, scope) {
|
|
459
|
+
return new AUIClient({
|
|
460
|
+
baseUrl: session.api_url || getBaseUrl(session.environment),
|
|
461
|
+
authToken: session.auth_token,
|
|
462
|
+
accountId: scope.accountId,
|
|
463
|
+
organizationId: scope.organizationId,
|
|
464
|
+
environment: session.environment,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async function resolveAgentManagementIdForScope(session, scope) {
|
|
468
|
+
if (scope.agentId)
|
|
469
|
+
return scope.agentId;
|
|
470
|
+
if (!scope.networkId || !scope.organizationId)
|
|
471
|
+
return undefined;
|
|
472
|
+
const client = buildAUIClient(session, scope);
|
|
473
|
+
try {
|
|
474
|
+
const resp = await client.agentManagement.listAgents(scope.organizationId, 1, 50, { network_id: scope.networkId });
|
|
475
|
+
const match = resp.items.find((agent) => agent.scope.network_id === scope.networkId || agent.id === scope.networkId);
|
|
476
|
+
return match?.id;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function resolveIntegrationAgentMode(session, scope) {
|
|
483
|
+
const override = process.env.AUI_FORCE_AGENT_MODE;
|
|
484
|
+
if (override === "bundle" || override === "records") {
|
|
485
|
+
return override;
|
|
486
|
+
}
|
|
487
|
+
const agentMgmtId = await resolveAgentManagementIdForScope(session, scope);
|
|
488
|
+
if (!agentMgmtId) {
|
|
489
|
+
return "records";
|
|
490
|
+
}
|
|
491
|
+
const resolution = await detectAgentBundleMode(buildAUIClient(session, scope), agentMgmtId);
|
|
492
|
+
return resolution.mode;
|
|
493
|
+
}
|
|
494
|
+
function persistIntegrationLocally(request) {
|
|
495
|
+
const projectRoot = findProjectRoot();
|
|
496
|
+
if (!projectRoot) {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
error: NO_PROJECT_ERROR,
|
|
500
|
+
suggestion: NO_PROJECT_SUGGESTION,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const code = integrationCodeFromName(request.name);
|
|
504
|
+
const integration = buildLocalMCPIntegrationRecord(request, code);
|
|
505
|
+
const filePath = mergeIntegrationIntoLocalFile(projectRoot, integration);
|
|
506
|
+
return {
|
|
507
|
+
ok: true,
|
|
508
|
+
code,
|
|
509
|
+
file: path.relative(projectRoot, filePath),
|
|
510
|
+
integration,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
323
513
|
/**
|
|
324
514
|
* Build the `IntegrationMCPSettings` payload posted to
|
|
325
515
|
* `/v1/integrations/view`. Switches on the discriminated union's `type`
|
|
@@ -368,13 +558,29 @@ function buildMCPSettings(request) {
|
|
|
368
558
|
}
|
|
369
559
|
}
|
|
370
560
|
export async function saveIntegration(session, request, scope) {
|
|
561
|
+
const agentMode = await resolveIntegrationAgentMode(session, scope);
|
|
562
|
+
const localPersist = persistIntegrationLocally(request);
|
|
563
|
+
if (!("ok" in localPersist)) {
|
|
564
|
+
return { ...localPersist, agentMode };
|
|
565
|
+
}
|
|
566
|
+
const localData = {
|
|
567
|
+
code: localPersist.code,
|
|
568
|
+
file: localPersist.file,
|
|
569
|
+
integration: localPersist.integration,
|
|
570
|
+
};
|
|
571
|
+
if (agentMode === "bundle") {
|
|
572
|
+
return {
|
|
573
|
+
success: true,
|
|
574
|
+
persistedVia: "local",
|
|
575
|
+
agentMode: "bundle",
|
|
576
|
+
suggestion: BUNDLE_MODE_LOCAL_SUGGESTION,
|
|
577
|
+
data: localData,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
371
580
|
const agentSettingsBase = getAgentSettingsWriteUrl(session.environment);
|
|
372
581
|
const url = `${agentSettingsBase}/v1/integrations/view`;
|
|
373
582
|
const categoryId = await resolveNetworkCategoryId(session, scope.networkId, scope);
|
|
374
|
-
const code =
|
|
375
|
-
.toLowerCase()
|
|
376
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
377
|
-
.replace(/^-|-$/g, "");
|
|
583
|
+
const code = localPersist.code;
|
|
378
584
|
// MCP integration settings — mirrors the backend variants
|
|
379
585
|
// (`agent_settings_transcoder/schemas/backend/integration.py`).
|
|
380
586
|
// `settings.type` discriminates between the two FLAT variants below;
|
|
@@ -439,9 +645,19 @@ export async function saveIntegration(session, request, scope) {
|
|
|
439
645
|
success: resp.ok,
|
|
440
646
|
});
|
|
441
647
|
if (!resp.ok) {
|
|
648
|
+
if (resp.status === 422 && isModeMismatchError(responseText)) {
|
|
649
|
+
return {
|
|
650
|
+
success: true,
|
|
651
|
+
persistedVia: "local",
|
|
652
|
+
agentMode: "bundle",
|
|
653
|
+
suggestion: BUNDLE_MODE_LOCAL_SUGGESTION,
|
|
654
|
+
data: localData,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
442
657
|
return {
|
|
443
658
|
success: false,
|
|
444
659
|
error: `Failed to create integration (${resp.status}): ${responseText}`,
|
|
660
|
+
agentMode: "records",
|
|
445
661
|
};
|
|
446
662
|
}
|
|
447
663
|
let parsed;
|
|
@@ -451,171 +667,95 @@ export async function saveIntegration(session, request, scope) {
|
|
|
451
667
|
catch {
|
|
452
668
|
parsed = responseText;
|
|
453
669
|
}
|
|
454
|
-
return {
|
|
670
|
+
return {
|
|
671
|
+
success: true,
|
|
672
|
+
data: { ...localData, server: parsed },
|
|
673
|
+
persistedVia: "local_and_server",
|
|
674
|
+
agentMode: "records",
|
|
675
|
+
};
|
|
455
676
|
}
|
|
456
|
-
// ─── Composio Native Integration ───
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
677
|
+
// ─── Composio Native Integration (via Apollo MCP API) ───
|
|
678
|
+
//
|
|
679
|
+
// Everything below is a thin orchestration layer on top of
|
|
680
|
+
// `/v1/integrations/mcp/composio/*` (see mcp-api.md). The CLI never
|
|
681
|
+
// holds a Composio API key — gateway auth (`Authorization: Bearer
|
|
682
|
+
// <session>`) is sufficient because Apollo owns the upstream Composio
|
|
683
|
+
// credential.
|
|
684
|
+
/** Build an Apollo client from a session + scope (used by Composio helpers and mcp-test). */
|
|
685
|
+
export function buildApolloClient(session, scope) {
|
|
686
|
+
return new ApolloClient({
|
|
687
|
+
authToken: session.auth_token,
|
|
688
|
+
organizationId: scope.organizationId,
|
|
689
|
+
accountId: scope.accountId,
|
|
690
|
+
userId: scope.userId,
|
|
691
|
+
environment: session.environment,
|
|
692
|
+
});
|
|
466
693
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
694
|
+
// ─── Toolkit listing ───
|
|
695
|
+
export async function listComposioToolkits(session, scope, options = {}) {
|
|
696
|
+
const client = buildApolloClient(session, scope);
|
|
697
|
+
const page = await client.listComposioToolkits({
|
|
698
|
+
query: options.search,
|
|
699
|
+
limit: options.limit ?? 50,
|
|
700
|
+
cursor: options.cursor,
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
items: page.items.map(toToolkitInfo),
|
|
704
|
+
nextCursor: page.next_cursor ?? undefined,
|
|
705
|
+
};
|
|
470
706
|
}
|
|
471
707
|
/**
|
|
472
|
-
* Fetch
|
|
473
|
-
*
|
|
708
|
+
* Fetch Composio tool catalog metadata by slug via Apollo
|
|
709
|
+
* (`GET /v1/integrations/mcp/composio/tools/by-slug`). No toolkit
|
|
710
|
+
* connection or `composio_user_id` required.
|
|
474
711
|
*/
|
|
475
|
-
export async function
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
method: "GET",
|
|
484
|
-
headers: {
|
|
485
|
-
"Content-Type": "application/json",
|
|
486
|
-
"auth-token": session.auth_token,
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
if (process.env.AUI_DEBUG) {
|
|
490
|
-
console.error(`[debug] Integration config response: ${resp.status}`);
|
|
491
|
-
}
|
|
492
|
-
if (!resp.ok)
|
|
493
|
-
return null;
|
|
494
|
-
const body = await resp.json();
|
|
495
|
-
const encodedKey = body?.data?.apiKey;
|
|
496
|
-
if (process.env.AUI_DEBUG) {
|
|
497
|
-
console.error(`[debug] Integration config: key present=${!!encodedKey}, label=${body?.data?.label || "none"}`);
|
|
498
|
-
}
|
|
499
|
-
if (typeof encodedKey === "string" && encodedKey.length > 0) {
|
|
500
|
-
try {
|
|
501
|
-
return Buffer.from(encodedKey, "base64").toString("utf-8");
|
|
502
|
-
}
|
|
503
|
-
catch {
|
|
504
|
-
return encodedKey;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
export async function listComposioToolkits(apiKey, options) {
|
|
514
|
-
const limit = options?.limit || 50;
|
|
515
|
-
const result = await fetchToolkitsViaREST(apiKey, limit, options?.search, options?.cursor);
|
|
516
|
-
const items = result.items.map((tk) => ({
|
|
517
|
-
slug: tk.slug || "",
|
|
518
|
-
name: tk.name || tk.slug || "",
|
|
519
|
-
logo: tk.meta?.logo || tk.logo || "",
|
|
520
|
-
description: tk.meta?.description || tk.description || "",
|
|
521
|
-
toolsCount: tk.meta?.toolsCount ?? tk.meta?.tools_count ?? tk.toolsCount ?? tk.tools_count ?? 0,
|
|
522
|
-
authSchemes: tk.authSchemes || tk.auth_schemes || [],
|
|
523
|
-
isNoAuth: tk.noAuth ?? tk.no_auth ?? false,
|
|
712
|
+
export async function fetchComposioToolsBySlugs(session, scope, slugs) {
|
|
713
|
+
const client = buildApolloClient(session, scope);
|
|
714
|
+
const tools = await client.listComposioToolsBySlug({ slugs });
|
|
715
|
+
return tools.map((t, i) => ({
|
|
716
|
+
slug: String(t.slug ?? slugs[i] ?? ""),
|
|
717
|
+
name: String(t.name ?? ""),
|
|
718
|
+
description: t.description ? String(t.description) : undefined,
|
|
719
|
+
input_schema: (t.input_schema ?? t.inputSchema),
|
|
524
720
|
}));
|
|
721
|
+
}
|
|
722
|
+
/** Project a raw Composio toolkit record onto the CLI's narrow view. */
|
|
723
|
+
function toToolkitInfo(tk) {
|
|
724
|
+
const meta = tk.meta ?? {};
|
|
725
|
+
const authConfigDetails = tk.auth_config_details ?? [];
|
|
525
726
|
return {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
727
|
+
slug: String(tk.slug ?? ""),
|
|
728
|
+
name: String(tk.name ?? tk.slug ?? ""),
|
|
729
|
+
logo: meta.logo ?? tk.logo,
|
|
730
|
+
description: meta.description ??
|
|
731
|
+
tk.description,
|
|
732
|
+
toolsCount: meta.tools_count ??
|
|
733
|
+
tk.tools_count,
|
|
734
|
+
authSchemes: authConfigDetails
|
|
735
|
+
.map((d) => d.mode)
|
|
736
|
+
.filter((m) => Boolean(m)),
|
|
737
|
+
isNoAuth: tk.no_auth ??
|
|
738
|
+
authConfigDetails.every((d) => d.mode === "NO_AUTH"),
|
|
529
739
|
};
|
|
530
740
|
}
|
|
531
|
-
|
|
532
|
-
let url = `https://backend.composio.dev/api/v3.1/toolkits?sort_by=usage&limit=${limit}`;
|
|
533
|
-
if (search)
|
|
534
|
-
url += `&search=${encodeURIComponent(search)}`;
|
|
535
|
-
if (cursor)
|
|
536
|
-
url += `&cursor=${encodeURIComponent(cursor)}`;
|
|
537
|
-
for (const headerName of ["x-api-key", "x-user-api-key"]) {
|
|
538
|
-
try {
|
|
539
|
-
const resp = await fetch(url, {
|
|
540
|
-
method: "GET",
|
|
541
|
-
headers: {
|
|
542
|
-
[headerName]: apiKey,
|
|
543
|
-
"Accept": "application/json",
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
if (process.env.AUI_DEBUG) {
|
|
547
|
-
console.error(`[debug] REST toolkits fetch with ${headerName}: status ${resp.status}`);
|
|
548
|
-
}
|
|
549
|
-
if (!resp.ok) {
|
|
550
|
-
if (process.env.AUI_DEBUG) {
|
|
551
|
-
const text = await resp.text();
|
|
552
|
-
console.error(`[debug] REST toolkits fetch failed (${resp.status}): ${text.slice(0, 200)}`);
|
|
553
|
-
}
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
const data = await resp.json();
|
|
557
|
-
if (process.env.AUI_DEBUG) {
|
|
558
|
-
console.error(`[debug] REST toolkits response keys: ${Object.keys(data).join(", ")}`);
|
|
559
|
-
console.error(`[debug] REST toolkits next_cursor: ${data.next_cursor ?? "none"}, total_items: ${data.total_items ?? "unknown"}, total_pages: ${data.total_pages ?? "unknown"}`);
|
|
560
|
-
}
|
|
561
|
-
const items = extractToolkitItems(data);
|
|
562
|
-
return {
|
|
563
|
-
items,
|
|
564
|
-
nextCursor: data.next_cursor || data.nextCursor || undefined,
|
|
565
|
-
totalPages: data.total_pages ?? data.totalPages ?? undefined,
|
|
566
|
-
totalItems: data.total_items ?? data.totalItems ?? undefined,
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
catch (e) {
|
|
570
|
-
if (process.env.AUI_DEBUG) {
|
|
571
|
-
console.error(`[debug] REST toolkits fetch error (${headerName}): ${e instanceof Error ? e.message : String(e)}`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
throw new Error("Failed to fetch toolkits. Check your Composio API key.");
|
|
576
|
-
}
|
|
577
|
-
function extractToolkitItems(resp) {
|
|
578
|
-
if (!resp)
|
|
579
|
-
return [];
|
|
580
|
-
if (Array.isArray(resp))
|
|
581
|
-
return resp;
|
|
582
|
-
if (Array.isArray(resp.items))
|
|
583
|
-
return resp.items;
|
|
584
|
-
if (Array.isArray(resp.data))
|
|
585
|
-
return resp.data;
|
|
586
|
-
if (resp.items && typeof resp.items === "object" && !Array.isArray(resp.items)) {
|
|
587
|
-
const vals = Object.values(resp.items);
|
|
588
|
-
if (vals.length > 0 && Array.isArray(vals[0]))
|
|
589
|
-
return vals[0];
|
|
590
|
-
}
|
|
591
|
-
for (const key of Object.keys(resp)) {
|
|
592
|
-
const val = resp[key];
|
|
593
|
-
if (Array.isArray(val) && val.length > 0 && val[0]?.slug) {
|
|
594
|
-
return val;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
return [];
|
|
598
|
-
}
|
|
599
|
-
// ─── Composio: Toolkit Auth Metadata ───
|
|
741
|
+
// ─── Toolkit auth metadata ───
|
|
600
742
|
const _OAUTH_SCHEMES = new Set(["OAUTH2", "OAUTH1", "DCR_OAUTH", "S2S_OAUTH2"]);
|
|
601
743
|
const _CREDENTIAL_SCHEMES = new Set(["API_KEY", "BEARER_TOKEN"]);
|
|
602
744
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
745
|
+
* Look up a single toolkit by slug (`/composio/toolkits?query=<slug>`)
|
|
746
|
+
* and surface the auth-scheme picker fields the CLI needs.
|
|
605
747
|
*/
|
|
606
|
-
export async function getComposioToolkitAuthInfo(
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
console.error(`[debug] toolkit raw keys: ${Object.keys(toolkit ?? {}).join(", ")}`);
|
|
613
|
-
console.error(`[debug] authConfigDetails: ${JSON.stringify(configDetails)}`);
|
|
614
|
-
console.error(`[debug] composioManagedAuthSchemes: ${JSON.stringify(managedSchemes)}`);
|
|
748
|
+
export async function getComposioToolkitAuthInfo(session, scope, toolkitSlug) {
|
|
749
|
+
const client = buildApolloClient(session, scope);
|
|
750
|
+
const page = await client.listComposioToolkits({ query: toolkitSlug, limit: 1 });
|
|
751
|
+
const toolkit = page.items[0];
|
|
752
|
+
if (!toolkit) {
|
|
753
|
+
throw new Error(`Toolkit '${toolkitSlug}' not found.`);
|
|
615
754
|
}
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
755
|
+
const configDetails = toolkit.auth_config_details ?? [];
|
|
756
|
+
const managedSchemes = toolkit.composio_managed_auth_schemes ?? [];
|
|
757
|
+
const isNoAuth = configDetails.length === 0 ||
|
|
758
|
+
configDetails.every((d) => d.mode === "NO_AUTH");
|
|
619
759
|
const schemes = configDetails
|
|
620
760
|
.map((d) => d.mode)
|
|
621
761
|
.filter((mode) => Boolean(mode) && mode !== "NO_AUTH")
|
|
@@ -625,48 +765,29 @@ export async function getComposioToolkitAuthInfo(apiKey, toolkitSlug) {
|
|
|
625
765
|
isOAuth: _OAUTH_SCHEMES.has(scheme),
|
|
626
766
|
isCredential: _CREDENTIAL_SCHEMES.has(scheme),
|
|
627
767
|
}));
|
|
628
|
-
|
|
629
|
-
if (process.env.AUI_DEBUG) {
|
|
630
|
-
console.error(`[debug] authInfo: defaultScheme=${defaultScheme}, isNoAuth=${isNoAuth}`);
|
|
631
|
-
console.error(`[debug] schemes: ${JSON.stringify(schemes)}`);
|
|
632
|
-
}
|
|
633
|
-
return { schemes, defaultScheme, isNoAuth };
|
|
634
|
-
}
|
|
635
|
-
// ─── Composio: Account & Auth-Config Helpers ───
|
|
636
|
-
export async function getComposioConnectedAccounts(apiKey, userId, toolkitSlug) {
|
|
637
|
-
const composio = await getComposioClient(apiKey);
|
|
638
|
-
return composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: [toolkitSlug] });
|
|
768
|
+
return { schemes, defaultScheme: schemes[0]?.scheme ?? "", isNoAuth };
|
|
639
769
|
}
|
|
640
770
|
/**
|
|
641
|
-
* Pre-flight
|
|
642
|
-
*
|
|
643
|
-
* `aui integration create`, or proceed
|
|
644
|
-
*
|
|
645
|
-
* - NO_AUTH toolkits → `{ isNoAuth: true, isConnected: true }` (nothing to do).
|
|
646
|
-
* - Auth-required toolkits → `isConnected` reflects whether the user already
|
|
647
|
-
* has an ACTIVE, non-disabled connected account for the toolkit (same
|
|
648
|
-
* predicate `resolveComposioToolkitAuth` uses to short-circuit).
|
|
649
|
-
*
|
|
650
|
-
* This catches the gap that `resolveComposioAuthConfigId` misses: a toolkit
|
|
651
|
-
* can have an auth-config provisioned globally (so MCP-server creation
|
|
652
|
-
* succeeds) while THIS user has not yet linked their account — in which
|
|
653
|
-
* case MCP tool calls would fail at runtime.
|
|
771
|
+
* Pre-flight: is this toolkit usable by this user? Combines toolkit
|
|
772
|
+
* metadata + connected-accounts listing so callers can decide whether
|
|
773
|
+
* to throw, redirect to `aui integration create`, or proceed.
|
|
654
774
|
*/
|
|
655
|
-
export async function getComposioToolkitConnectionStatus(
|
|
656
|
-
const
|
|
657
|
-
const
|
|
775
|
+
export async function getComposioToolkitConnectionStatus(session, scope, composioUserId, toolkitSlug) {
|
|
776
|
+
const client = buildApolloClient(session, scope);
|
|
777
|
+
const page = await client.listComposioToolkits({ query: toolkitSlug, limit: 1 });
|
|
778
|
+
const toolkit = page.items[0];
|
|
658
779
|
if (!toolkit) {
|
|
659
780
|
throw new Error(`Toolkit '${toolkitSlug}' not found.`);
|
|
660
781
|
}
|
|
661
|
-
const authMode = toolkit.
|
|
782
|
+
const authMode = toolkit.auth_config_details?.[0]?.mode;
|
|
662
783
|
if (!authMode || authMode === "NO_AUTH") {
|
|
663
784
|
return { isNoAuth: true, isConnected: true };
|
|
664
785
|
}
|
|
665
|
-
const accounts = await
|
|
666
|
-
|
|
667
|
-
|
|
786
|
+
const accounts = await client.listComposioConnectedAccounts({
|
|
787
|
+
composioUserId,
|
|
788
|
+
toolkit: toolkitSlug,
|
|
668
789
|
});
|
|
669
|
-
const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.
|
|
790
|
+
const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.is_disabled);
|
|
670
791
|
return {
|
|
671
792
|
isNoAuth: false,
|
|
672
793
|
authMode,
|
|
@@ -674,385 +795,105 @@ export async function getComposioToolkitConnectionStatus(apiKey, userId, toolkit
|
|
|
674
795
|
connectedAccountId: active?.id,
|
|
675
796
|
};
|
|
676
797
|
}
|
|
798
|
+
// ─── Authorize + wait ───
|
|
677
799
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* - Short-circuits when the user already has an ACTIVE connection.
|
|
681
|
-
* - **BYOD path** (`authCredentials` or `authConfigId` supplied): creates a
|
|
682
|
-
* CUSTOM auth config with the provided credentials, then links the account
|
|
683
|
-
* directly via `connectedAccounts.link()`.
|
|
684
|
-
* - **Standard OAuth path** (no custom credentials): finds or creates a
|
|
685
|
-
* COMPOSIO_MANAGED auth config, then links directly via `connectedAccounts.link()`.
|
|
686
|
-
* Session creation is skipped — the MCP URL is generated server-side.
|
|
800
|
+
* Start (or reuse) a toolkit connection. `redirectUrl` is empty when the
|
|
801
|
+
* connection is already ACTIVE — callers should skip the browser flow.
|
|
687
802
|
*/
|
|
688
|
-
export async function
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (authConfigId) {
|
|
708
|
-
resolvedConfigId = authConfigId;
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
if (!authScheme) {
|
|
712
|
-
throw new Error(`authScheme is required when providing custom credentials for toolkit '${toolkitSlug}'.`);
|
|
803
|
+
export async function connectComposioToolkit(session, scope, composioUserId, toolkitSlug, options = {}) {
|
|
804
|
+
const client = buildApolloClient(session, scope);
|
|
805
|
+
const result = await client.authorizeComposioToolkit({
|
|
806
|
+
composio_user_id: composioUserId,
|
|
807
|
+
toolkit: toolkitSlug,
|
|
808
|
+
...(options.authScheme ? { auth_scheme: options.authScheme } : {}),
|
|
809
|
+
...(options.authCredentials
|
|
810
|
+
? {
|
|
811
|
+
auth_credentials: {
|
|
812
|
+
...(options.authCredentials.clientId != null && {
|
|
813
|
+
client_id: options.authCredentials.clientId,
|
|
814
|
+
}),
|
|
815
|
+
...(options.authCredentials.clientSecret != null && {
|
|
816
|
+
client_secret: options.authCredentials.clientSecret,
|
|
817
|
+
}),
|
|
818
|
+
...(options.authCredentials.bearerToken != null && {
|
|
819
|
+
bearer_token: options.authCredentials.bearerToken,
|
|
820
|
+
}),
|
|
821
|
+
},
|
|
713
822
|
}
|
|
714
|
-
|
|
715
|
-
...(authCredentials.clientId != null && { client_id: authCredentials.clientId }),
|
|
716
|
-
...(authCredentials.clientSecret != null && { client_secret: authCredentials.clientSecret }),
|
|
717
|
-
...(authCredentials.bearerToken != null && { generic_id: authCredentials.bearerToken }),
|
|
718
|
-
};
|
|
719
|
-
const created = await composio.authConfigs.create(toolkitSlug, {
|
|
720
|
-
type: AuthConfigTypes.CUSTOM,
|
|
721
|
-
authScheme,
|
|
722
|
-
credentials,
|
|
723
|
-
});
|
|
724
|
-
resolvedConfigId = created.id;
|
|
725
|
-
}
|
|
726
|
-
return composio.connectedAccounts.link(userId, resolvedConfigId, callbackUrl != null ? { callbackUrl } : undefined);
|
|
727
|
-
}
|
|
728
|
-
// ── Standard path: find or create an auth config ──
|
|
729
|
-
// Session creation is unnecessary here — the integration stores composio.user_id
|
|
730
|
-
// and toolkit slugs; the MCP URL is generated server-side. Using authConfigs +
|
|
731
|
-
// connectedAccounts.link() directly avoids any session-creation API limitations.
|
|
732
|
-
const existingConfigs = await composio.authConfigs.list({ toolkit: toolkitSlug });
|
|
733
|
-
const match = existingConfigs.items.find((c) => c.status === "ENABLED" && (!c.authScheme || !authScheme || c.authScheme === authScheme));
|
|
734
|
-
if (process.env.AUI_DEBUG) {
|
|
735
|
-
console.error(`[debug] resolveComposioToolkitAuth match: ${JSON.stringify(match)}`);
|
|
736
|
-
}
|
|
737
|
-
let resolvedConfigId;
|
|
738
|
-
if (match) {
|
|
739
|
-
resolvedConfigId = match.id;
|
|
740
|
-
}
|
|
741
|
-
else {
|
|
742
|
-
// Determine auth config type from the toolkit's schema:
|
|
743
|
-
// use COMPOSIO_MANAGED only when the scheme is Composio-managed,
|
|
744
|
-
// otherwise fall back to CUSTOM (e.g. API_KEY or non-managed OAuth schemes).
|
|
745
|
-
// Prefer the caller-supplied `isManaged` flag (avoids an extra API call) and
|
|
746
|
-
// only fetch the toolkit when the caller didn't provide it.
|
|
747
|
-
let isManaged;
|
|
748
|
-
if (options.isManaged !== undefined) {
|
|
749
|
-
isManaged = options.isManaged;
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
const toolkit = await composio.toolkits.get(toolkitSlug);
|
|
753
|
-
const managedSchemes = toolkit.composioManagedAuthSchemes ?? [];
|
|
754
|
-
const effectiveScheme = authScheme ?? toolkit.authConfigDetails?.[0]?.mode;
|
|
755
|
-
isManaged = effectiveScheme ? managedSchemes.includes(effectiveScheme) : managedSchemes.length > 0;
|
|
756
|
-
}
|
|
757
|
-
if (process.env.AUI_DEBUG) {
|
|
758
|
-
console.error(`[debug] resolveComposioToolkitAuth isManaged=${isManaged}, authScheme=${authScheme}`);
|
|
759
|
-
}
|
|
760
|
-
let created;
|
|
761
|
-
if (isManaged) {
|
|
762
|
-
// COMPOSIO_MANAGED: type literal only — authScheme is not part of this schema variant.
|
|
763
|
-
created = await composio.authConfigs.create(toolkitSlug, {
|
|
764
|
-
type: AuthConfigTypes.COMPOSIO_MANAGED,
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
// Non-managed (e.g. API_KEY): create a CUSTOM config with the resolved scheme.
|
|
769
|
-
// Credentials were not supplied by the caller — pass an empty object and let
|
|
770
|
-
// the Composio API return its own validation error if they are required.
|
|
771
|
-
created = await composio.authConfigs.create(toolkitSlug, {
|
|
772
|
-
type: AuthConfigTypes.CUSTOM,
|
|
773
|
-
authScheme: authScheme,
|
|
774
|
-
credentials: {},
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
resolvedConfigId = created.id;
|
|
778
|
-
}
|
|
779
|
-
return composio.connectedAccounts.link(userId, resolvedConfigId, callbackUrl != null ? { callbackUrl } : undefined);
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* High-level connection helper:
|
|
783
|
-
* - For BYOD (custom `authCredentials`): fetches the toolkit's first supported
|
|
784
|
-
* auth scheme, then delegates to the BYOD path of `resolveComposioToolkitAuth`.
|
|
785
|
-
* - For standard OAuth: delegates directly to `resolveComposioToolkitAuth`
|
|
786
|
-
* which uses a COMPOSIO_MANAGED auth config + `connectedAccounts.link()`.
|
|
787
|
-
*
|
|
788
|
-
* Returns the `ConnectionRequest` alongside the standard `AuthorizationResult`
|
|
789
|
-
* fields so callers can invoke `waitForComposioConnection`.
|
|
790
|
-
*/
|
|
791
|
-
export async function connectComposioToolkit(apiKey, userId, toolkitSlug, options = {}) {
|
|
792
|
-
let authScheme = options.authScheme;
|
|
793
|
-
if (!authScheme && options.authCredentials) {
|
|
794
|
-
// Only fetch toolkit metadata when BYO credentials are given without an explicit scheme.
|
|
795
|
-
const composio = await getComposioClient(apiKey);
|
|
796
|
-
const toolkit = await composio.toolkits.get(toolkitSlug);
|
|
797
|
-
const supportedModes = (toolkit.authConfigDetails ?? []).map((d) => d.mode);
|
|
798
|
-
if (supportedModes.length === 0) {
|
|
799
|
-
throw new Error(`Toolkit '${toolkitSlug}' has no supported auth schemes.`);
|
|
800
|
-
}
|
|
801
|
-
authScheme = supportedModes[0];
|
|
802
|
-
}
|
|
803
|
-
const req = await resolveComposioToolkitAuth(apiKey, userId, toolkitSlug, {
|
|
804
|
-
callbackUrl: options.callbackUrl,
|
|
805
|
-
authCredentials: options.authCredentials,
|
|
806
|
-
authScheme,
|
|
807
|
-
isManaged: options.isManaged,
|
|
823
|
+
: {}),
|
|
808
824
|
});
|
|
809
825
|
return {
|
|
810
|
-
id:
|
|
811
|
-
status:
|
|
812
|
-
redirectUrl:
|
|
813
|
-
connectionRequest: req,
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
export async function waitForComposioConnection(authResult, timeout = 120000) {
|
|
817
|
-
const connectedAccount = await authResult.connectionRequest.waitForConnection(timeout);
|
|
818
|
-
return {
|
|
819
|
-
id: connectedAccount.id || "",
|
|
820
|
-
status: connectedAccount.status || "active",
|
|
826
|
+
id: result.id,
|
|
827
|
+
status: result.status,
|
|
828
|
+
redirectUrl: result.redirect_url ?? "",
|
|
821
829
|
};
|
|
822
830
|
}
|
|
823
831
|
/**
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
* toolkit requires auth but no auth-configs have been provisioned yet —
|
|
827
|
-
* callers should run `connectComposioToolkit` first.
|
|
832
|
+
* Poll `/composio/connected-accounts` until an ACTIVE entry appears for
|
|
833
|
+
* the `(user, toolkit)` pair, or `timeoutMs` elapses.
|
|
828
834
|
*/
|
|
829
|
-
async function
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const server = await composio.mcp.create(name, {
|
|
847
|
-
toolkits: [{ toolkit, ...(authConfigId && { authConfigId }) }],
|
|
848
|
-
allowedTools,
|
|
849
|
-
});
|
|
850
|
-
return server.id;
|
|
851
|
-
}
|
|
852
|
-
/** True when `existing` matches the desired toolkit + tool-set exactly. */
|
|
853
|
-
function isComposioMcpServerInSync(existing, toolkit, allowedTools) {
|
|
854
|
-
if (existing.toolkits.length !== 1 || existing.toolkits[0] !== toolkit) {
|
|
855
|
-
return false;
|
|
835
|
+
export async function waitForComposioConnection(session, scope, composioUserId, toolkitSlug, timeoutMs = 120_000) {
|
|
836
|
+
const client = buildApolloClient(session, scope);
|
|
837
|
+
const deadline = Date.now() + timeoutMs;
|
|
838
|
+
const intervalMs = 2_000;
|
|
839
|
+
while (Date.now() < deadline) {
|
|
840
|
+
const accounts = await client.listComposioConnectedAccounts({
|
|
841
|
+
composioUserId,
|
|
842
|
+
toolkit: toolkitSlug,
|
|
843
|
+
});
|
|
844
|
+
const active = accounts.items.find((a) => a.status === "ACTIVE" && !a.is_disabled);
|
|
845
|
+
if (active) {
|
|
846
|
+
return {
|
|
847
|
+
id: String(active.id ?? ""),
|
|
848
|
+
status: String(active.status ?? "ACTIVE"),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
856
852
|
}
|
|
857
|
-
|
|
858
|
-
return (saved.size === allowedTools.length &&
|
|
859
|
-
allowedTools.every((t) => saved.has(t)));
|
|
853
|
+
throw new Error(`Timed out waiting for Composio connection on toolkit '${toolkitSlug}'.`);
|
|
860
854
|
}
|
|
855
|
+
// ─── MCP server provisioning ───
|
|
861
856
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
857
|
+
* Provision (or reuse) a Composio MCP server scoped to `(user, toolkit,
|
|
858
|
+
* allowed_tools)` and return its HTTP URL + caller-stable `serverId`.
|
|
859
|
+
* Backend returns 400 when the toolkit requires auth and no connection
|
|
860
|
+
* exists — callers should `connectComposioToolkit` first.
|
|
866
861
|
*/
|
|
867
|
-
export async function
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
862
|
+
export async function getComposioServerUrl(session, scope, params) {
|
|
863
|
+
const client = buildApolloClient(session, scope);
|
|
864
|
+
const result = await client.getComposioServerUrl({
|
|
865
|
+
composio_user_id: params.composioUserId,
|
|
866
|
+
toolkit: params.toolkit,
|
|
867
|
+
allowed_tools: params.allowedTools,
|
|
868
|
+
...(params.serverId ? { server_id: params.serverId } : {}),
|
|
874
869
|
});
|
|
875
|
-
|
|
876
|
-
if (!existing)
|
|
877
|
-
return createComposioMcpServer(composio, spec);
|
|
878
|
-
if (isComposioMcpServerInSync(existing, spec.toolkit, spec.allowedTools)) {
|
|
879
|
-
return existing.id;
|
|
880
|
-
}
|
|
881
|
-
if (process.env.AUI_DEBUG) {
|
|
882
|
-
console.error(`[debug] Composio MCP server drifted for id=${existing.id}, recreating`);
|
|
883
|
-
}
|
|
884
|
-
await composio.mcp.delete(existing.id);
|
|
885
|
-
return createComposioMcpServer(composio, spec);
|
|
870
|
+
return { url: result.url, serverId: result.server_id };
|
|
886
871
|
}
|
|
887
872
|
/**
|
|
888
|
-
*
|
|
873
|
+
* List tools exposed by a Composio toolkit for the given user. The user
|
|
874
|
+
* must already be authorised — call `connectComposioToolkit` first.
|
|
889
875
|
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* `serverId` is whatever was used (input or generated), NOT Composio's
|
|
894
|
-
* internal config id — callers persist it so subsequent calls resolve
|
|
895
|
-
* the same MCP server.
|
|
876
|
+
* Backend returns the same `MCPToolSchema` shape as `/mcp/list`, so
|
|
877
|
+
* `MCPTool.input_schema` (snake_case) is populated for both DIRECT and
|
|
878
|
+
* COMPOSIO paths consistently.
|
|
896
879
|
*/
|
|
897
|
-
export async function
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const mcpConfigId = await getOrCreateComposioMcpServerId(composio, {
|
|
903
|
-
toolkit,
|
|
904
|
-
name: serverId,
|
|
905
|
-
allowedTools,
|
|
906
|
-
authConfigId,
|
|
880
|
+
export async function discoverComposioTools(session, scope, composioUserId, toolkitSlug) {
|
|
881
|
+
const client = buildApolloClient(session, scope);
|
|
882
|
+
const tools = await client.listComposioTools({
|
|
883
|
+
composioUserId,
|
|
884
|
+
toolkit: toolkitSlug,
|
|
907
885
|
});
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
let url = `https://backend.composio.dev/api/v3.1/tools?toolkit_slug=${encodeURIComponent(toolkitSlug)}&limit=${limit}`;
|
|
921
|
-
if (options?.query)
|
|
922
|
-
url += `&query=${encodeURIComponent(options.query)}`;
|
|
923
|
-
if (options?.cursor)
|
|
924
|
-
url += `&cursor=${encodeURIComponent(options.cursor)}`;
|
|
925
|
-
for (const headerName of ["x-api-key", "x-user-api-key"]) {
|
|
926
|
-
const resp = await fetch(url, {
|
|
927
|
-
method: "GET",
|
|
928
|
-
headers: { [headerName]: apiKey, "Accept": "application/json" },
|
|
929
|
-
});
|
|
930
|
-
if (!resp.ok)
|
|
931
|
-
continue;
|
|
932
|
-
const data = await resp.json();
|
|
933
|
-
if (process.env.AUI_DEBUG) {
|
|
934
|
-
console.error(`[debug] Composio tools response keys: ${Object.keys(data).join(", ")}`);
|
|
935
|
-
console.error(`[debug] Composio tools count: ${data.items?.length ?? data.length ?? "unknown"}`);
|
|
936
|
-
}
|
|
937
|
-
return {
|
|
938
|
-
tools: extractComposioTools(data),
|
|
939
|
-
nextCursor: data?.next_cursor || data?.nextCursor || undefined,
|
|
940
|
-
totalItems: data?.total_items ?? data?.totalItems ?? undefined,
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
throw new Error(`Failed to fetch tools for ${toolkitSlug}. Check your API key.`);
|
|
944
|
-
}
|
|
945
|
-
function extractComposioTools(data) {
|
|
946
|
-
let rawItems = [];
|
|
947
|
-
if (Array.isArray(data))
|
|
948
|
-
rawItems = data;
|
|
949
|
-
else if (Array.isArray(data?.items))
|
|
950
|
-
rawItems = data.items;
|
|
951
|
-
else if (Array.isArray(data?.data))
|
|
952
|
-
rawItems = data.data;
|
|
953
|
-
return rawItems.map((t) => ({
|
|
954
|
-
name: t.slug || t.name || "",
|
|
955
|
-
description: t.description || t.human_description || "",
|
|
956
|
-
inputSchema: t.input_parameters || t.inputParameters || undefined,
|
|
957
|
-
}));
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* Fetch full Composio tool descriptors by their slugs. Mirrors the
|
|
961
|
-
* dashboard request
|
|
962
|
-
* GET https://backend.composio.dev/api/v3.1/tools
|
|
963
|
-
* ?toolkit_versions=<version>&tool_slugs=<SLUG_A,SLUG_B>
|
|
964
|
-
*
|
|
965
|
-
* Returns the raw items from the response, so callers (notably the
|
|
966
|
-
* `aui integration tools --json` command) can surface every field
|
|
967
|
-
* Composio sends — input/output schemas, toolkit metadata, tags, etc.
|
|
968
|
-
*
|
|
969
|
-
* Auth: the same Composio API key the CLI already uses (auto-fetched
|
|
970
|
-
* via `fetchComposioApiKey`). Both `x-api-key` and `x-user-api-key`
|
|
971
|
-
* header names are tried so this works against both org and user keys.
|
|
972
|
-
*/
|
|
973
|
-
export async function fetchComposioToolsBySlugs(apiKey, toolSlugs, options) {
|
|
974
|
-
if (!apiKey) {
|
|
975
|
-
throw new Error("Composio API key is required");
|
|
976
|
-
}
|
|
977
|
-
if (!Array.isArray(toolSlugs) || toolSlugs.length === 0) {
|
|
978
|
-
throw new Error("At least one tool slug is required");
|
|
979
|
-
}
|
|
980
|
-
const params = new URLSearchParams();
|
|
981
|
-
if (options?.toolkitVersions) {
|
|
982
|
-
params.set("toolkit_versions", options.toolkitVersions);
|
|
983
|
-
}
|
|
984
|
-
params.set("tool_slugs", toolSlugs.join(","));
|
|
985
|
-
if (typeof options?.limit === "number") {
|
|
986
|
-
params.set("limit", String(options.limit));
|
|
987
|
-
}
|
|
988
|
-
if (options?.cursor) {
|
|
989
|
-
params.set("cursor", options.cursor);
|
|
990
|
-
}
|
|
991
|
-
const url = `https://backend.composio.dev/api/v3.1/tools?${params.toString()}`;
|
|
992
|
-
let lastError = null;
|
|
993
|
-
// Composio accepts the key under either of these headers depending on
|
|
994
|
-
// whether the key was minted as an org key (x-api-key) or a user key
|
|
995
|
-
// (x-user-api-key). The other helpers in this file (toolkits, tools
|
|
996
|
-
// by toolkit) probe both — keep behavior consistent here.
|
|
997
|
-
for (const headerName of ["x-api-key", "x-user-api-key"]) {
|
|
998
|
-
try {
|
|
999
|
-
const resp = await fetch(url, {
|
|
1000
|
-
method: "GET",
|
|
1001
|
-
headers: {
|
|
1002
|
-
[headerName]: apiKey,
|
|
1003
|
-
Accept: "application/json",
|
|
1004
|
-
},
|
|
1005
|
-
});
|
|
1006
|
-
if (process.env.AUI_DEBUG) {
|
|
1007
|
-
console.error(`[debug] fetchComposioToolsBySlugs (${headerName}): status ${resp.status}`);
|
|
1008
|
-
}
|
|
1009
|
-
if (!resp.ok) {
|
|
1010
|
-
const text = await resp.text();
|
|
1011
|
-
lastError = new Error(`HTTP ${resp.status}: ${text.slice(0, 500)}`);
|
|
1012
|
-
// Auth-style failures — try the alternate header name before giving up.
|
|
1013
|
-
if (resp.status === 401 || resp.status === 403)
|
|
1014
|
-
continue;
|
|
1015
|
-
throw lastError;
|
|
1016
|
-
}
|
|
1017
|
-
const data = await resp.json();
|
|
1018
|
-
if (process.env.AUI_DEBUG) {
|
|
1019
|
-
console.error(`[debug] fetchComposioToolsBySlugs response keys: ${Object.keys(data || {}).join(", ")}`);
|
|
1020
|
-
}
|
|
1021
|
-
const rawItems = extractToolDetailItems(data);
|
|
1022
|
-
return {
|
|
1023
|
-
tools: rawItems,
|
|
1024
|
-
nextCursor: data?.next_cursor || data?.nextCursor || undefined,
|
|
1025
|
-
totalItems: data?.total_items ?? data?.totalItems ?? undefined,
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
catch (e) {
|
|
1029
|
-
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1030
|
-
if (process.env.AUI_DEBUG) {
|
|
1031
|
-
console.error(`[debug] fetchComposioToolsBySlugs error (${headerName}): ${lastError.message}`);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
throw (lastError ??
|
|
1036
|
-
new Error("Failed to fetch tools by slugs from Composio. Check your API key."));
|
|
1037
|
-
}
|
|
1038
|
-
function extractToolDetailItems(data) {
|
|
1039
|
-
if (!data)
|
|
1040
|
-
return [];
|
|
1041
|
-
if (Array.isArray(data))
|
|
1042
|
-
return data;
|
|
1043
|
-
if (Array.isArray(data.items))
|
|
1044
|
-
return data.items;
|
|
1045
|
-
if (Array.isArray(data.data))
|
|
1046
|
-
return data.data;
|
|
1047
|
-
if (Array.isArray(data.tools))
|
|
1048
|
-
return data.tools;
|
|
1049
|
-
// Walk one level for any nested array whose first item looks tool-ish.
|
|
1050
|
-
for (const key of Object.keys(data)) {
|
|
1051
|
-
const val = data[key];
|
|
1052
|
-
if (Array.isArray(val) && val.length > 0 && (val[0]?.slug || val[0]?.name)) {
|
|
1053
|
-
return val;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
return [];
|
|
886
|
+
return {
|
|
887
|
+
tools: tools.map((t) => ({
|
|
888
|
+
name: String(t.name ?? ""),
|
|
889
|
+
description: String(t.description ?? ""),
|
|
890
|
+
...(t.input_schema
|
|
891
|
+
? { input_schema: t.input_schema }
|
|
892
|
+
: {}),
|
|
893
|
+
...(t.inputSchema
|
|
894
|
+
? { inputSchema: t.inputSchema }
|
|
895
|
+
: {}),
|
|
896
|
+
})),
|
|
897
|
+
};
|
|
1057
898
|
}
|
|
1058
899
|
//# sourceMappingURL=integration.service.js.map
|