agent-trellis 0.10.0 → 0.12.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/dist/adapters/mcpPlan.d.ts +1 -0
- package/dist/adapters/mcpPlan.js +14 -5
- package/dist/cli.js +30 -5
- package/dist/commands/mcp.d.ts +71 -0
- package/dist/commands/mcp.js +145 -4
- package/dist/commands/mcpAuth.d.ts +6 -0
- package/dist/commands/mcpAuth.js +38 -10
- package/dist/commands/mcpGateway.js +7 -7
- package/dist/commands/skill.d.ts +45 -1
- package/dist/commands/skill.js +83 -2
- package/dist/core/canonical.d.ts +1 -0
- package/dist/core/canonical.js +8 -3
- package/dist/core/types.d.ts +4 -1
- package/dist/core/types.js +3 -0
- package/dist/lib/agentBridgeProvider.d.ts +1 -1
- package/dist/lib/gatewayBackend.d.ts +11 -0
- package/dist/lib/gatewayBackend.js +23 -4
- package/dist/lib/mcpConnect.d.ts +6 -3
- package/dist/lib/mcpConnect.js +8 -6
- package/dist/lib/mcpRuntime.d.ts +12 -2
- package/dist/lib/mcpRuntime.js +70 -9
- package/dist/lib/mcpStatusProvider.d.ts +1 -1
- package/dist/lib/memoryProvider.d.ts +1 -1
- package/dist/lib/oauth/discovery.d.ts +1 -0
- package/dist/lib/oauth/discovery.js +2 -0
- package/dist/lib/oauth/flow.d.ts +4 -0
- package/dist/lib/oauth/flow.js +48 -11
- package/dist/lib/oauth/refresh.d.ts +1 -0
- package/dist/lib/oauth/refresh.js +3 -1
- package/dist/lib/oauth/store.d.ts +2 -0
- package/dist/lib/runtimeControlProvider.d.ts +1 -1
- package/dist/lib/scopeSelection.d.ts +36 -0
- package/dist/lib/scopeSelection.js +65 -0
- package/dist/lib/skillManifest.d.ts +66 -0
- package/dist/lib/skillManifest.js +203 -0
- package/dist/lib/skillProvider.d.ts +21 -1
- package/dist/lib/skillProvider.js +61 -1
- package/dist/lib/taskProvider.d.ts +1 -1
- package/dist/lib/upstreamResources.d.ts +23 -0
- package/dist/lib/upstreamResources.js +192 -0
- package/dist/pi-bridge/bundle.js +37906 -19932
- package/docs/architecture.md +14 -13
- package/docs/mcp-authorization-ownership.md +59 -0
- package/docs/skills-over-mcp.md +91 -0
- package/package.json +6 -3
|
@@ -34,6 +34,7 @@ export declare const RUNTIME_COMMAND = "trellis";
|
|
|
34
34
|
* routing it exists to carry (trellis-mcp-oauth-static-client design.md
|
|
35
35
|
* D1). */
|
|
36
36
|
export declare function isOAuthMcpServer(def: McpServerDef): boolean;
|
|
37
|
+
export declare function isAgentOwnedOAuth(def: McpServerDef): boolean;
|
|
37
38
|
/**
|
|
38
39
|
* Gateway mode applies to every managed agent when `agents` is omitted —
|
|
39
40
|
* the expected normal case (design.md D14). An explicit list narrows it,
|
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { isInScope } from "../core/adapter.js";
|
|
20
20
|
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
21
|
-
import { isOAuthAuth } from "../core/types.js";
|
|
21
|
+
import { isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
22
22
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
23
23
|
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
24
24
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
@@ -40,6 +40,9 @@ export const RUNTIME_COMMAND = "trellis";
|
|
|
40
40
|
export function isOAuthMcpServer(def) {
|
|
41
41
|
return isOAuthAuth(def.auth);
|
|
42
42
|
}
|
|
43
|
+
export function isAgentOwnedOAuth(def) {
|
|
44
|
+
return isOAuthMcpServer(def) && oauthOwner(def.auth) === "agent";
|
|
45
|
+
}
|
|
43
46
|
/**
|
|
44
47
|
* Gateway mode applies to every managed agent when `agents` is omitted —
|
|
45
48
|
* the expected normal case (design.md D14). An explicit list narrows it,
|
|
@@ -158,7 +161,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
158
161
|
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
|
|
159
162
|
};
|
|
160
163
|
}
|
|
161
|
-
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route,
|
|
164
|
+
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
162
165
|
// Every ordinary per-server check (enabled, scope, literal secrets,
|
|
163
166
|
// unresolved env names, Codex's header shape) still runs at gateway
|
|
164
167
|
// startup. OAuth servers are the deliberate exception: they are written
|
|
@@ -234,15 +237,21 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
234
237
|
// Runtime mode owns the agent-facing MCP edge. It resolves the same
|
|
235
238
|
// direct/gateway-compatible upstream route at startup, so native
|
|
236
239
|
// per-server entries are not duplicated here.
|
|
240
|
+
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
237
241
|
return {
|
|
238
242
|
desired: [{
|
|
239
243
|
name: RUNTIME_ENTRY_NAME,
|
|
240
244
|
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
|
|
241
|
-
}],
|
|
242
|
-
conflicts:
|
|
245
|
+
}, ...oauth.desired],
|
|
246
|
+
conflicts: oauth.conflicts,
|
|
243
247
|
};
|
|
244
248
|
}
|
|
245
|
-
|
|
249
|
+
const native = planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
250
|
+
const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
|
|
251
|
+
return {
|
|
252
|
+
desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
|
|
253
|
+
conflicts: [...native.conflicts, ...hosted.map(({ name }) => ({ name, message: `Trellis-owned OAuth server "${name}" requires gateway or runtime delivery for ${agentId}`, remediation: "enable gateway/runtime delivery or set auth.owner to agent" }))],
|
|
254
|
+
};
|
|
246
255
|
}
|
|
247
256
|
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
248
257
|
const desired = [];
|
package/dist/cli.js
CHANGED
|
@@ -9,13 +9,14 @@ import { runInit } from "./commands/init.js";
|
|
|
9
9
|
import { runMigrate } from "./commands/migrate.js";
|
|
10
10
|
import { runOnboard } from "./commands/onboard.js";
|
|
11
11
|
import { runSync } from "./commands/sync.js";
|
|
12
|
-
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, runMcpSetAuth, parseMcpAddArgs, parseMcpSetArgs } from "./commands/mcp.js";
|
|
12
|
+
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, runMcpSetAuth, runMcpScope, parseMcpAddArgs, parseMcpSetArgs } from "./commands/mcp.js";
|
|
13
|
+
import { parseScopeArgs } from "./lib/scopeSelection.js";
|
|
13
14
|
import { runMcpImport } from "./commands/mcpImport.js";
|
|
14
15
|
import { parseMcpGatewayArgs, runMcpGateway, runMcpRuntime } from "./commands/mcpGateway.js";
|
|
15
16
|
import { runMcpAuth } from "./commands/mcpAuth.js";
|
|
16
17
|
import { runSecretsAudit } from "./commands/secretsAudit.js";
|
|
17
18
|
import { runRollback } from "./commands/rollback.js";
|
|
18
|
-
import { runSkillList, runSkillAdd, runSkillRemove, runSkillUpdateBuiltin } from "./commands/skill.js";
|
|
19
|
+
import { runSkillList, runSkillAdd, runSkillRemove, runSkillScope, runSkillUpdateBuiltin } from "./commands/skill.js";
|
|
19
20
|
import { runRemoteSkillAdd, runRemoteSkillList, runRemoteSkillUpdate } from "./commands/remoteSkill.js";
|
|
20
21
|
import { collectMemoryList, runMemoryExtraction, runMemorySync } from "./commands/memory.js";
|
|
21
22
|
import { parseManageArgs, runManage } from "./commands/manage.js";
|
|
@@ -132,7 +133,7 @@ Commands:
|
|
|
132
133
|
--json machine-readable output, no report text
|
|
133
134
|
mcp set <name> --auth oauth|none
|
|
134
135
|
Set or clear explicit OAuth classification; canonical only.
|
|
135
|
-
--auth oauth [--client-id <id>] [--client-secret-env <NAME>]
|
|
136
|
+
--auth oauth [--auth-owner agent|trellis] [--client-id <id>] [--client-secret-env <NAME>]
|
|
136
137
|
pre-register a client for a provider that refuses
|
|
137
138
|
dynamic registration. --client-id is the PROVIDER's
|
|
138
139
|
published identifier (public by design);
|
|
@@ -142,6 +143,14 @@ Commands:
|
|
|
142
143
|
--auth none removes it along with the classification.
|
|
143
144
|
--dry-run preview the plan, write nothing
|
|
144
145
|
--json machine-readable output, no report text
|
|
146
|
+
mcp scope <name> --agents <ids> | --all | --none
|
|
147
|
+
Choose which managed agents reach a canonical MCP server, then
|
|
148
|
+
run the same sync as mcp sync. Exactly one selector is
|
|
149
|
+
required; ids must be managed agents. Selecting every managed
|
|
150
|
+
agent records no explicit scope, so a newly managed agent is
|
|
151
|
+
included. An entry Trellis did not write is never removed.
|
|
152
|
+
--dry-run preview the plan, write nothing
|
|
153
|
+
--json machine-readable output, no report text
|
|
145
154
|
mcp auth <server-name>
|
|
146
155
|
Authorize a remote (http/sse) MCP server that uses OAuth —
|
|
147
156
|
opens a browser once, stores the result 0600 under
|
|
@@ -169,6 +178,12 @@ Commands:
|
|
|
169
178
|
--dry-run preview the plan, write nothing
|
|
170
179
|
--json machine-readable output, no report text
|
|
171
180
|
skill remove <name>
|
|
181
|
+
skill scope <name> --agents <ids> | --all | --none
|
|
182
|
+
Choose which managed agents receive a canonical skill, then
|
|
183
|
+
sync it. Same selector rules as mcp scope; a built-in skill's
|
|
184
|
+
scope is not editable
|
|
185
|
+
--dry-run preview the plan, write nothing
|
|
186
|
+
--json machine-readable output, no report text
|
|
172
187
|
skill update-builtin
|
|
173
188
|
Refresh the package-owned trellis-runtime Skill in canonical
|
|
174
189
|
source; existing native symlinks see the update
|
|
@@ -440,6 +455,11 @@ async function main(argv) {
|
|
|
440
455
|
process.exitCode = runMcpSetAuth(name, parseMcpSetArgs(mcpRest), { json, dryRun }).exitCode;
|
|
441
456
|
return;
|
|
442
457
|
}
|
|
458
|
+
if (subcommand === "scope") {
|
|
459
|
+
const [name] = mcpRest;
|
|
460
|
+
process.exitCode = (await runMcpScope(name && !name.startsWith("--") ? name : undefined, parseScopeArgs(mcpRest), { json, dryRun })).exitCode;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
443
463
|
if (subcommand === "remove") {
|
|
444
464
|
const [name] = mcpRest;
|
|
445
465
|
if (!name) {
|
|
@@ -461,7 +481,7 @@ async function main(argv) {
|
|
|
461
481
|
process.exitCode = exitCode;
|
|
462
482
|
return;
|
|
463
483
|
}
|
|
464
|
-
console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync|list|add <name>|set <name>|remove <name>|auth <name>\n`);
|
|
484
|
+
console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync|list|add <name>|set <name>|scope <name>|remove <name>|auth <name>\n`);
|
|
465
485
|
process.exitCode = 1;
|
|
466
486
|
return;
|
|
467
487
|
}
|
|
@@ -495,11 +515,16 @@ async function main(argv) {
|
|
|
495
515
|
process.exitCode = (await runSkillRemove(name, { json, dryRun })).exitCode;
|
|
496
516
|
return;
|
|
497
517
|
}
|
|
518
|
+
if (subcommand === "scope") {
|
|
519
|
+
const [name] = skillRest;
|
|
520
|
+
process.exitCode = (await runSkillScope(name && !name.startsWith("--") ? name : undefined, parseScopeArgs(skillRest), { json, dryRun })).exitCode;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
498
523
|
if (subcommand === "update-builtin") {
|
|
499
524
|
process.exitCode = runSkillUpdateBuiltin({ json, dryRun }).exitCode;
|
|
500
525
|
return;
|
|
501
526
|
}
|
|
502
|
-
console.error(`Unknown skill subcommand: ${subcommand ?? "(none)"}\nUsage: trellis skill list|add <name> --from <path>|remove <name>|update-builtin\n`);
|
|
527
|
+
console.error(`Unknown skill subcommand: ${subcommand ?? "(none)"}\nUsage: trellis skill list|add <name> --from <path>|remove <name>|scope <name> --agents <ids>|--all|--none|update-builtin\n`);
|
|
503
528
|
process.exitCode = 1;
|
|
504
529
|
return;
|
|
505
530
|
}
|
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { type ServersYamlWriteResult } from "../core/canonical.js";
|
|
16
16
|
import type { AdapterPlanItem } from "../core/adapter.js";
|
|
17
17
|
import type { AgentId, McpAuthMode, McpConfig, McpServerDef, Transport } from "../core/types.js";
|
|
18
|
+
import { type ScopeSelectorRaw } from "../lib/scopeSelection.js";
|
|
18
19
|
import { type BackupSession } from "../lib/backup.js";
|
|
19
20
|
export interface RunMcpSyncOptions {
|
|
20
21
|
json?: boolean;
|
|
@@ -55,6 +56,12 @@ export interface McpListEntry {
|
|
|
55
56
|
/** Normalized: `oauth` in either the scalar or the object form reports as
|
|
56
57
|
* the scalar here — the listing reports classification, not metadata. */
|
|
57
58
|
auth?: McpAuthMode;
|
|
59
|
+
authOwner?: "agent" | "trellis";
|
|
60
|
+
agentAuthorization?: Array<{
|
|
61
|
+
agent: AgentId;
|
|
62
|
+
status: "unknown";
|
|
63
|
+
instruction: string;
|
|
64
|
+
}>;
|
|
58
65
|
/** Presence of pre-registered client metadata
|
|
59
66
|
* (trellis-mcp-oauth-static-client task 3.2) — `true` when the object form
|
|
60
67
|
* carries a `client_id`. A presence flag, deliberately not the id itself:
|
|
@@ -62,6 +69,12 @@ export interface McpListEntry {
|
|
|
62
69
|
* ever changes and there is one less place a future secret-looking field
|
|
63
70
|
* could leak through. */
|
|
64
71
|
preRegisteredClient?: true;
|
|
72
|
+
/** State of the stored OAuth credential, for OAuth-classified servers only
|
|
73
|
+
* (trellis-scope-editing-and-auth-status design.md D6). It describes the
|
|
74
|
+
* credential, not the connection: an `authorized` server can still be down. */
|
|
75
|
+
authStatus?: McpAuthStatus;
|
|
76
|
+
/** Epoch ms, when the stored token advertises an expiry. */
|
|
77
|
+
authExpiresAt?: number;
|
|
65
78
|
enabled: boolean;
|
|
66
79
|
agents: readonly AgentId[];
|
|
67
80
|
command?: string;
|
|
@@ -77,6 +90,18 @@ export interface McpListEntry {
|
|
|
77
90
|
* printed in full. */
|
|
78
91
|
staticEnv?: Record<string, string>;
|
|
79
92
|
}
|
|
93
|
+
export type McpAuthStatus = "not-authorized" | "authorized" | "refreshable" | "expired" | "unknown";
|
|
94
|
+
/**
|
|
95
|
+
* Reads only the per-server token store and returns the state, never a
|
|
96
|
+
* token. Classification stays explicit — the caller only asks for servers
|
|
97
|
+
* already declared OAuth, so a stray token file never makes one. A server
|
|
98
|
+
* name that cannot be a token filename, or an unreadable file, simply has no
|
|
99
|
+
* credential: one odd entry must not take down the whole listing.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mcpCredentialState(homeDir: string, serverName: string, resourceUrl?: string): {
|
|
102
|
+
authStatus: McpAuthStatus;
|
|
103
|
+
authExpiresAt?: number;
|
|
104
|
+
};
|
|
80
105
|
export declare function collectMcpListPlan(homeDir?: string): McpListEntry[];
|
|
81
106
|
export declare function runMcpList(opts?: {
|
|
82
107
|
homeDir?: string;
|
|
@@ -137,6 +162,7 @@ export interface McpSetAuthPlan {
|
|
|
137
162
|
* plan collector. */
|
|
138
163
|
export interface McpSetAuthRawArgs {
|
|
139
164
|
auth?: string;
|
|
165
|
+
authOwner?: string;
|
|
140
166
|
/** Public literal by design (design.md D2) — the one field it is fine to
|
|
141
167
|
* pass on a command line. Never echoed back in output or errors. */
|
|
142
168
|
clientId?: string;
|
|
@@ -169,6 +195,51 @@ export declare function runMcpSetAuth(name: string | undefined, raw: McpSetAuthR
|
|
|
169
195
|
}): {
|
|
170
196
|
exitCode: number;
|
|
171
197
|
};
|
|
198
|
+
export declare function applyMcpSetAuthWithSync(plan: McpSetAuthPlan, homeDir: string): Promise<{
|
|
199
|
+
plan: McpSetAuthPlan;
|
|
200
|
+
sync?: McpSyncReport;
|
|
201
|
+
}>;
|
|
202
|
+
export type McpScopeAction = "updated" | "already-set" | "not-found" | "invalid-input";
|
|
203
|
+
export interface McpScopePlan {
|
|
204
|
+
name: string;
|
|
205
|
+
action: McpScopeAction;
|
|
206
|
+
detail: string;
|
|
207
|
+
/** Recorded as `agents:` on the entry; `undefined` removes the key so the
|
|
208
|
+
* server reaches every managed agent. */
|
|
209
|
+
scope?: AgentId[];
|
|
210
|
+
mode?: "all" | "none" | "explicit";
|
|
211
|
+
normalizedFromFull?: boolean;
|
|
212
|
+
effective?: readonly AgentId[];
|
|
213
|
+
/** Only set when action === "updated"; consumed by applyMcpScopeWithSync. */
|
|
214
|
+
def?: McpServerDef;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `mcp scope <name> --agents a,b | --all | --none`
|
|
218
|
+
* (trellis-scope-editing-and-auth-status tasks.md 1.2). Same selector rules as
|
|
219
|
+
* `skill scope`, which is why both go through `resolveScopeSelection`.
|
|
220
|
+
*/
|
|
221
|
+
export declare function collectMcpScopePlan(name: string | undefined, raw: ScopeSelectorRaw, homeDir?: string): McpScopePlan;
|
|
222
|
+
export interface McpScopeOutcome {
|
|
223
|
+
plan: McpScopePlan;
|
|
224
|
+
writeError?: string;
|
|
225
|
+
sync?: McpSyncReport;
|
|
226
|
+
}
|
|
227
|
+
/** Write inside a backup session, then the same `mcp sync` `add` / `remove`
|
|
228
|
+
* run. Removal of an entry from an agent narrowed away stays gated by the
|
|
229
|
+
* ownership ledger, so an entry Trellis did not write is never deleted
|
|
230
|
+
* (design.md D5). */
|
|
231
|
+
export declare function applyMcpScopeWithSync(plan: McpScopePlan, opts?: {
|
|
232
|
+
homeDir?: string;
|
|
233
|
+
dryRun?: boolean;
|
|
234
|
+
backupSession?: BackupSession;
|
|
235
|
+
}): Promise<McpScopeOutcome>;
|
|
236
|
+
export declare function runMcpScope(name: string | undefined, raw: ScopeSelectorRaw, opts?: {
|
|
237
|
+
homeDir?: string;
|
|
238
|
+
json?: boolean;
|
|
239
|
+
dryRun?: boolean;
|
|
240
|
+
}): Promise<{
|
|
241
|
+
exitCode: number;
|
|
242
|
+
}>;
|
|
172
243
|
export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string, backup?: BackupSession): ServersYamlWriteResult;
|
|
173
244
|
export interface McpAddOutcome {
|
|
174
245
|
plan: McpAddPlan;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -16,8 +16,10 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { loadCanonicalSource, upsertServerYaml, removeServerYaml } from "../core/canonical.js";
|
|
19
|
-
import { ALL_AGENTS, isOAuthAuth, oauthClientMetadata, resolveScope } from "../core/types.js";
|
|
19
|
+
import { ALL_AGENTS, isOAuthAuth, oauthClientMetadata, oauthOwner, resolveScope } from "../core/types.js";
|
|
20
20
|
import { isValidSecretVarName } from "../lib/secretEnv.js";
|
|
21
|
+
import { isExpired, readToken } from "../lib/oauth/store.js";
|
|
22
|
+
import { describeScope, resolveScopeSelection, sameScope } from "../lib/scopeSelection.js";
|
|
21
23
|
import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
|
|
22
24
|
import { CodexAdapter } from "../adapters/codex.js";
|
|
23
25
|
import { KiroAdapter } from "../adapters/kiro.js";
|
|
@@ -114,6 +116,34 @@ export function printReport(report, dryRun) {
|
|
|
114
116
|
function serversYamlPath(homeDir) {
|
|
115
117
|
return join(homeDir, ".trellis", "mcp", "servers.yaml");
|
|
116
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Reads only the per-server token store and returns the state, never a
|
|
121
|
+
* token. Classification stays explicit — the caller only asks for servers
|
|
122
|
+
* already declared OAuth, so a stray token file never makes one. A server
|
|
123
|
+
* name that cannot be a token filename, or an unreadable file, simply has no
|
|
124
|
+
* credential: one odd entry must not take down the whole listing.
|
|
125
|
+
*/
|
|
126
|
+
export function mcpCredentialState(homeDir, serverName, resourceUrl) {
|
|
127
|
+
let token;
|
|
128
|
+
try {
|
|
129
|
+
token = readToken(homeDir, serverName);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { authStatus: "not-authorized" };
|
|
133
|
+
}
|
|
134
|
+
if (!token?.accessToken)
|
|
135
|
+
return { authStatus: "not-authorized" };
|
|
136
|
+
if (resourceUrl && token.resourceUrl) {
|
|
137
|
+
const resource = new URL(resourceUrl);
|
|
138
|
+
resource.hash = "";
|
|
139
|
+
if (token.resourceUrl !== resource.toString())
|
|
140
|
+
return { authStatus: "not-authorized" };
|
|
141
|
+
}
|
|
142
|
+
const expiry = token.expiresAt !== undefined ? { authExpiresAt: token.expiresAt } : {};
|
|
143
|
+
if (!isExpired(token))
|
|
144
|
+
return { authStatus: "authorized", ...expiry };
|
|
145
|
+
return { authStatus: token.refreshToken ? "refreshable" : "expired", ...expiry };
|
|
146
|
+
}
|
|
117
147
|
export function collectMcpListPlan(homeDir = homedir()) {
|
|
118
148
|
const canonical = loadCanonicalSource(homeDir);
|
|
119
149
|
return Object.entries(canonical.mcp.servers).map(([name, def]) => ({
|
|
@@ -121,6 +151,16 @@ export function collectMcpListPlan(homeDir = homedir()) {
|
|
|
121
151
|
transport: def.transport,
|
|
122
152
|
auth: isOAuthAuth(def.auth) ? "oauth" : undefined,
|
|
123
153
|
preRegisteredClient: oauthClientMetadata(def.auth).clientId !== undefined ? true : undefined,
|
|
154
|
+
...(isOAuthAuth(def.auth) ? {
|
|
155
|
+
authOwner: oauthOwner(def.auth),
|
|
156
|
+
...(oauthOwner(def.auth) === "trellis" ? mcpCredentialState(homeDir, name, def.url) : {
|
|
157
|
+
authStatus: "unknown",
|
|
158
|
+
agentAuthorization: resolveScope(def.agents, canonical.managedAgents).map((agent) => ({
|
|
159
|
+
agent, status: "unknown",
|
|
160
|
+
instruction: agent === "codex" ? `codex mcp login ${name}` : agent === "claude-code" ? "Open Claude Code and use /mcp to authorize this server" : "Authorize this server in the Agent's MCP settings",
|
|
161
|
+
})),
|
|
162
|
+
}),
|
|
163
|
+
} : {}),
|
|
124
164
|
enabled: def.enabled ?? true,
|
|
125
165
|
agents: resolveScope(def.agents, canonical.managedAgents),
|
|
126
166
|
command: def.command,
|
|
@@ -150,7 +190,7 @@ export function runMcpList(opts = {}) {
|
|
|
150
190
|
else {
|
|
151
191
|
for (const e of entries) {
|
|
152
192
|
const scope = e.agents.length > 0 ? e.agents.join(", ") : "(no managed agent reaches it)";
|
|
153
|
-
console.log(`${e.name} (${e.transport}${e.auth === "oauth" ? ", oauth" : ""}${e.preRegisteredClient ? ", pre-registered client" : ""})${e.enabled ? "" : " [disabled]"} — ${scope}`);
|
|
193
|
+
console.log(`${e.name} (${e.transport}${e.auth === "oauth" ? ", oauth" : ""}${e.preRegisteredClient ? ", pre-registered client" : ""}${e.authStatus ? `, credential: ${e.authStatus}` : ""})${e.enabled ? "" : " [disabled]"} — ${scope}`);
|
|
154
194
|
if (e.env && e.env.length > 0)
|
|
155
195
|
console.log(` env: ${e.env.join(", ")} (values never read/printed)`);
|
|
156
196
|
if (e.staticEnv)
|
|
@@ -186,6 +226,7 @@ export function parseMcpSetArgs(argv) {
|
|
|
186
226
|
auth: flag("--auth"),
|
|
187
227
|
clientId: flag("--client-id"),
|
|
188
228
|
clientSecretEnv: flag("--client-secret-env"),
|
|
229
|
+
authOwner: flag("--auth-owner"),
|
|
189
230
|
};
|
|
190
231
|
}
|
|
191
232
|
function parseCsv(raw) {
|
|
@@ -289,8 +330,10 @@ export function collectMcpSetAuthPlan(name, raw, homeDir = homedir()) {
|
|
|
289
330
|
const mode = raw.auth === "oauth" || raw.auth === "none" ? raw.auth : undefined;
|
|
290
331
|
if (!name)
|
|
291
332
|
return { name: "(none)", auth: (mode ?? "none"), action: "invalid-input", detail: "usage: trellis mcp set <name> --auth oauth|none [--client-id <id>] [--client-secret-env <NAME>]" };
|
|
292
|
-
const hasClientFlags = raw.clientId !== undefined || raw.clientSecretEnv !== undefined;
|
|
293
|
-
const clientFlagsDetail =
|
|
333
|
+
const hasClientFlags = raw.clientId !== undefined || raw.clientSecretEnv !== undefined || raw.authOwner !== undefined;
|
|
334
|
+
const clientFlagsDetail = raw.authOwner !== undefined
|
|
335
|
+
? "--auth-owner requires an explicit --auth oauth in the same command"
|
|
336
|
+
: "--client-id / --client-secret-env require an explicit --auth oauth in the same command";
|
|
294
337
|
if (mode === undefined) {
|
|
295
338
|
// Naming the missing --auth is the actionable error when that is what
|
|
296
339
|
// the user was reaching for; otherwise report the invalid value itself.
|
|
@@ -304,6 +347,9 @@ export function collectMcpSetAuthPlan(name, raw, homeDir = homedir()) {
|
|
|
304
347
|
if (raw.clientId === "") {
|
|
305
348
|
return { name, auth: mode, action: "invalid-input", detail: "--client-id must not be empty" };
|
|
306
349
|
}
|
|
350
|
+
if (raw.authOwner !== undefined && raw.authOwner !== "agent" && raw.authOwner !== "trellis") {
|
|
351
|
+
return { name, auth: mode, action: "invalid-input", detail: "--auth-owner must be agent or trellis" };
|
|
352
|
+
}
|
|
307
353
|
if (raw.clientSecretEnv !== undefined && !isValidSecretVarName(raw.clientSecretEnv)) {
|
|
308
354
|
// The value is deliberately not echoed: a name-shaped field failing
|
|
309
355
|
// this check is at best a typo and at worst a pasted credential
|
|
@@ -362,6 +408,7 @@ export function collectMcpSetAuthPlan(name, raw, homeDir = homedir()) {
|
|
|
362
408
|
...current,
|
|
363
409
|
auth: {
|
|
364
410
|
kind: "oauth",
|
|
411
|
+
...(raw.authOwner !== undefined ? { owner: raw.authOwner } : typeof current.auth === "object" && current.auth.owner !== undefined ? { owner: current.auth.owner } : {}),
|
|
365
412
|
...(clientId !== undefined ? { clientId } : {}),
|
|
366
413
|
...(clientSecretEnv !== undefined ? { clientSecretEnv } : {}),
|
|
367
414
|
},
|
|
@@ -399,6 +446,100 @@ export function runMcpSetAuth(name, raw, opts = {}) {
|
|
|
399
446
|
}
|
|
400
447
|
return { exitCode: ["invalid-input", "not-found"].includes(plan.action) || Boolean(writeError) ? 1 : 0 };
|
|
401
448
|
}
|
|
449
|
+
export async function applyMcpSetAuthWithSync(plan, homeDir) {
|
|
450
|
+
if (plan.action !== "updated" || !plan.def)
|
|
451
|
+
return { plan };
|
|
452
|
+
const backup = openBackupSession(homeDir, "mcp-auth-owner");
|
|
453
|
+
try {
|
|
454
|
+
const path = serversYamlPath(homeDir);
|
|
455
|
+
const result = upsertServerYaml(path, plan.name, plan.def, backup);
|
|
456
|
+
if (!result.ok)
|
|
457
|
+
throw new Error(result.error);
|
|
458
|
+
return { plan, sync: await collectMcpSyncReport({ homeDir, backupSession: backup }) };
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
backup.finalize();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `mcp scope <name> --agents a,b | --all | --none`
|
|
466
|
+
* (trellis-scope-editing-and-auth-status tasks.md 1.2). Same selector rules as
|
|
467
|
+
* `skill scope`, which is why both go through `resolveScopeSelection`.
|
|
468
|
+
*/
|
|
469
|
+
export function collectMcpScopePlan(name, raw, homeDir = homedir()) {
|
|
470
|
+
if (!name)
|
|
471
|
+
return { name: "(none)", action: "invalid-input", detail: "usage: trellis mcp scope <name> --agents <ids> | --all | --none" };
|
|
472
|
+
const canonical = loadCanonicalSource(homeDir);
|
|
473
|
+
const selection = resolveScopeSelection(raw, canonical.managedAgents);
|
|
474
|
+
if (!selection.ok)
|
|
475
|
+
return { name, action: "invalid-input", detail: selection.detail };
|
|
476
|
+
const current = canonical.mcp.servers[name];
|
|
477
|
+
if (!current)
|
|
478
|
+
return { name, action: "not-found", detail: `no canonical MCP server named "${name}"` };
|
|
479
|
+
const base = {
|
|
480
|
+
name,
|
|
481
|
+
scope: selection.scope,
|
|
482
|
+
mode: selection.mode,
|
|
483
|
+
normalizedFromFull: selection.normalizedFromFull,
|
|
484
|
+
effective: resolveScope(selection.scope, canonical.managedAgents),
|
|
485
|
+
};
|
|
486
|
+
const summary = describeScope(selection.scope, canonical.managedAgents);
|
|
487
|
+
if (sameScope(current.agents, selection.scope)) {
|
|
488
|
+
return { ...base, action: "already-set", detail: `scope is already ${summary}` };
|
|
489
|
+
}
|
|
490
|
+
const { agents: _agents, ...withoutAgents } = current;
|
|
491
|
+
const def = selection.scope === undefined ? withoutAgents : { ...withoutAgents, agents: selection.scope };
|
|
492
|
+
const normalized = selection.normalizedFromFull ? " (every managed agent selected — recorded as the default, so a newly managed agent is included)" : "";
|
|
493
|
+
return { ...base, action: "updated", detail: `set scope to ${summary}${normalized}`, def };
|
|
494
|
+
}
|
|
495
|
+
/** Write inside a backup session, then the same `mcp sync` `add` / `remove`
|
|
496
|
+
* run. Removal of an entry from an agent narrowed away stays gated by the
|
|
497
|
+
* ownership ledger, so an entry Trellis did not write is never deleted
|
|
498
|
+
* (design.md D5). */
|
|
499
|
+
export async function applyMcpScopeWithSync(plan, opts = {}) {
|
|
500
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
501
|
+
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "updated" ? openBackupSession(homeDir, "mcp-scope") : undefined;
|
|
502
|
+
const session = opts.backupSession ?? ownSession;
|
|
503
|
+
let writeError;
|
|
504
|
+
if (!opts.dryRun && plan.action === "updated" && plan.def) {
|
|
505
|
+
const result = upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def, session);
|
|
506
|
+
if (!result.ok)
|
|
507
|
+
writeError = result.error;
|
|
508
|
+
}
|
|
509
|
+
ownSession?.finalize();
|
|
510
|
+
let syncReport;
|
|
511
|
+
if (!writeError && plan.action === "updated" && existsSync(join(homeDir, ".trellis"))) {
|
|
512
|
+
syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
|
|
513
|
+
}
|
|
514
|
+
return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
|
|
515
|
+
}
|
|
516
|
+
export async function runMcpScope(name, raw, opts = {}) {
|
|
517
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
518
|
+
let outcome;
|
|
519
|
+
try {
|
|
520
|
+
outcome = await applyMcpScopeWithSync(collectMcpScopePlan(name, raw, homeDir), { homeDir, dryRun: opts.dryRun });
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
524
|
+
return { exitCode: 1 };
|
|
525
|
+
}
|
|
526
|
+
const { plan, writeError, sync: syncReport } = outcome;
|
|
527
|
+
if (opts.json) {
|
|
528
|
+
const payload = writeError ? { ...plan, writeError } : plan;
|
|
529
|
+
console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}mcp scope ${plan.name}`);
|
|
533
|
+
console.log(` [${plan.action}] ${plan.detail}`);
|
|
534
|
+
if (writeError)
|
|
535
|
+
console.error(` write failed: ${writeError}`);
|
|
536
|
+
if (syncReport)
|
|
537
|
+
printReport(syncReport, opts.dryRun ?? false);
|
|
538
|
+
}
|
|
539
|
+
const failed = plan.action === "invalid-input" || plan.action === "not-found" || Boolean(writeError);
|
|
540
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
541
|
+
return { exitCode: failed || syncConflict ? 1 : 0 };
|
|
542
|
+
}
|
|
402
543
|
export function applyMcpAddPlan(plan, homeDir = homedir(), backup) {
|
|
403
544
|
if (plan.action !== "create" || !plan.def)
|
|
404
545
|
return { ok: true };
|
|
@@ -16,6 +16,11 @@ export interface RunMcpAuthOptions {
|
|
|
16
16
|
homeDir?: string;
|
|
17
17
|
/** Test seam — the real one launches the platform browser. */
|
|
18
18
|
openBrowser?: (url: string) => void | Promise<void>;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
onProgress?: (phase: "discovering" | "registering" | "waiting" | "exchanging" | "saving") => void;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Used by the desktop; native grants belong to each Agent. */
|
|
23
|
+
requireHosted?: boolean;
|
|
19
24
|
}
|
|
20
25
|
export interface McpAuthResult {
|
|
21
26
|
server: string;
|
|
@@ -26,3 +31,4 @@ export interface McpAuthResult {
|
|
|
26
31
|
export declare function runMcpAuth(opts: RunMcpAuthOptions): Promise<{
|
|
27
32
|
exitCode: number;
|
|
28
33
|
}>;
|
|
34
|
+
export declare function authorizeMcpServer(opts: RunMcpAuthOptions): Promise<McpAuthResult>;
|
package/dist/commands/mcpAuth.js
CHANGED
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
13
13
|
import { discoverAuthorizationServer, parseResourceMetadataUrl } from "../lib/oauth/discovery.js";
|
|
14
|
-
import { authorize } from "../lib/oauth/flow.js";
|
|
14
|
+
import { authorize, abortable } from "../lib/oauth/flow.js";
|
|
15
15
|
import { ensureFreshToken } from "../lib/oauth/refresh.js";
|
|
16
16
|
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
17
17
|
import { isExpired, readToken, tokenPath, writeToken } from "../lib/oauth/store.js";
|
|
18
|
-
import {
|
|
18
|
+
import { withServerLock } from "../lib/oauth/lock.js";
|
|
19
|
+
import { isOAuthAuth, oauthOwner, oauthClientMetadata } from "../core/types.js";
|
|
19
20
|
export async function runMcpAuth(opts) {
|
|
20
21
|
const homeDir = opts.homeDir ?? homedir();
|
|
21
22
|
let result;
|
|
22
23
|
try {
|
|
23
|
-
result = await
|
|
24
|
+
result = await authorizeMcpServer(opts);
|
|
24
25
|
}
|
|
25
26
|
catch (err) {
|
|
26
27
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -41,7 +42,9 @@ export async function runMcpAuth(opts) {
|
|
|
41
42
|
}
|
|
42
43
|
return { exitCode: 0 };
|
|
43
44
|
}
|
|
44
|
-
async function
|
|
45
|
+
export async function authorizeMcpServer(opts) {
|
|
46
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
47
|
+
opts.signal?.throwIfAborted();
|
|
45
48
|
const canonical = loadCanonicalSource(homeDir);
|
|
46
49
|
const def = canonical.mcp.servers[opts.serverName];
|
|
47
50
|
if (!def) {
|
|
@@ -54,15 +57,21 @@ async function authorizeServer(opts, homeDir) {
|
|
|
54
57
|
// flow that does not exist.
|
|
55
58
|
throw new Error(`MCP server "${opts.serverName}" is a stdio server — OAuth applies to http/sse servers. Its credentials come from \`env\`/\`env_aliases\` and secrets.policy.yaml.`);
|
|
56
59
|
}
|
|
60
|
+
if (opts.requireHosted && (!isOAuthAuth(def.auth) || oauthOwner(def.auth) !== "trellis" || def.enabled === false)) {
|
|
61
|
+
throw new Error("This connection is authorized by its Agent; choose explicit Trellis ownership before starting hosted authorization");
|
|
62
|
+
}
|
|
57
63
|
const file = tokenPath(homeDir, opts.serverName);
|
|
64
|
+
const resourceUrl = new URL(def.url);
|
|
65
|
+
resourceUrl.hash = "";
|
|
58
66
|
if (!opts.force) {
|
|
59
|
-
const
|
|
67
|
+
const stored = readToken(homeDir, opts.serverName);
|
|
68
|
+
const existing = stored?.resourceUrl && stored.resourceUrl !== resourceUrl.toString() ? undefined : stored;
|
|
60
69
|
if (existing && !isExpired(existing)) {
|
|
61
70
|
// Re-running is a no-op rather than a fresh grant: rotating a
|
|
62
71
|
// perfectly good credential is a way to break a working setup.
|
|
63
72
|
return { server: opts.serverName, status: "already-valid", tokenFile: file, ...(existing.expiresAt ? { expiresAt: existing.expiresAt } : {}) };
|
|
64
73
|
}
|
|
65
|
-
if (existing?.refreshToken) {
|
|
74
|
+
if (existing?.refreshToken && !opts.requireHosted) {
|
|
66
75
|
// Expired but renewable — no browser needed.
|
|
67
76
|
const refreshed = await ensureFreshToken(homeDir, opts.serverName);
|
|
68
77
|
if (refreshed) {
|
|
@@ -70,7 +79,10 @@ async function authorizeServer(opts, homeDir) {
|
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
|
-
|
|
82
|
+
opts.onProgress?.("discovering");
|
|
83
|
+
const fetchImpl = (url, init) => fetch(url, { ...init, signal: opts.signal });
|
|
84
|
+
const metadata = await abortable(discoverAuthorizationServer(def.url, { wwwAuthenticate: await probeWwwAuthenticate(def, opts.signal), fetchImpl }), opts.signal);
|
|
85
|
+
opts.signal?.throwIfAborted();
|
|
74
86
|
if (!metadata) {
|
|
75
87
|
throw new Error(`could not discover OAuth metadata for "${opts.serverName}" (${def.url}) — it may not use OAuth, or may not publish RFC 8414/9728 metadata`);
|
|
76
88
|
}
|
|
@@ -90,12 +102,28 @@ async function authorizeServer(opts, homeDir) {
|
|
|
90
102
|
clientSecret = value;
|
|
91
103
|
}
|
|
92
104
|
const token = await authorize(opts.serverName, metadata, {
|
|
105
|
+
signal: opts.signal,
|
|
106
|
+
resourceUrl: resourceUrl.toString(),
|
|
107
|
+
onProgress: opts.onProgress,
|
|
108
|
+
timeoutMs: opts.timeoutMs,
|
|
93
109
|
...(opts.openBrowser ? { openBrowser: opts.openBrowser } : {}),
|
|
94
110
|
...(metadata.scopesSupported?.length ? { scope: metadata.scopesSupported.join(" ") } : {}),
|
|
95
111
|
...(client.clientId !== undefined ? { clientId: client.clientId } : {}),
|
|
96
112
|
...(clientSecret !== undefined ? { clientSecret } : {}),
|
|
97
113
|
});
|
|
98
|
-
|
|
114
|
+
opts.signal?.throwIfAborted();
|
|
115
|
+
// Serialize the final write with silent refreshes, without holding a lock
|
|
116
|
+
// while the user is in the browser. An older refresh cannot overwrite a new
|
|
117
|
+
// grant after it is committed.
|
|
118
|
+
opts.onProgress?.("saving");
|
|
119
|
+
await withServerLock(homeDir, opts.serverName, async () => {
|
|
120
|
+
opts.signal?.throwIfAborted();
|
|
121
|
+
const current = loadCanonicalSource(homeDir).mcp.servers[opts.serverName];
|
|
122
|
+
if (!current || current.url !== def.url || JSON.stringify(current.auth) !== JSON.stringify(def.auth) || current.enabled === false) {
|
|
123
|
+
throw new Error("The server's endpoint or authorization configuration changed; start authorization again");
|
|
124
|
+
}
|
|
125
|
+
writeToken(homeDir, opts.serverName, token);
|
|
126
|
+
});
|
|
99
127
|
return { server: opts.serverName, status: "authorized", tokenFile: file, ...(token.expiresAt ? { expiresAt: token.expiresAt } : {}) };
|
|
100
128
|
}
|
|
101
129
|
/**
|
|
@@ -113,9 +141,9 @@ function secretSourceLabel(policy) {
|
|
|
113
141
|
* server directly, which beats probing well-known paths. Failure is not
|
|
114
142
|
* an error: discovery falls back to probing.
|
|
115
143
|
*/
|
|
116
|
-
async function probeWwwAuthenticate(def) {
|
|
144
|
+
async function probeWwwAuthenticate(def, signal) {
|
|
117
145
|
try {
|
|
118
|
-
const response = await fetch(def.url, { method: "GET" });
|
|
146
|
+
const response = await fetch(def.url, { method: "GET", signal });
|
|
119
147
|
if (response.status !== 401)
|
|
120
148
|
return null;
|
|
121
149
|
const header = response.headers.get("www-authenticate");
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* an in-process backend without this file, or any agent's config,
|
|
15
15
|
* changing.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
20
|
-
import { isGatewayAgent,
|
|
20
|
+
import { isGatewayAgent, isAgentOwnedOAuth, resolveMcpPlan } from "../adapters/mcpPlan.js";
|
|
21
21
|
import { LocalBackend } from "../lib/gatewayBackend.js";
|
|
22
22
|
import { DEFAULT_CONNECT_TIMEOUT_MS } from "../lib/mcpConnect.js";
|
|
23
23
|
import { BuiltinRegistry, createRuntimeServer, UpstreamProvider } from "../lib/mcpRuntime.js";
|
|
@@ -65,7 +65,9 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
65
65
|
const directRoutes = canonical.mcp.routes && route
|
|
66
66
|
? { ...canonical.mcp.routes, [agentId]: { mode: "direct", ...(route.servers ? { servers: route.servers } : {}) } }
|
|
67
67
|
: canonical.mcp.routes;
|
|
68
|
-
const ordinaryServers = Object.fromEntries(Object.entries(canonical.mcp.servers)
|
|
68
|
+
const ordinaryServers = Object.fromEntries(Object.entries(canonical.mcp.servers)
|
|
69
|
+
.filter(([, def]) => !isAgentOwnedOAuth(def))
|
|
70
|
+
.map(([name, def]) => { const { auth: _auth, ...ordinary } = def; return [name, ordinary]; }));
|
|
69
71
|
const direct = { ...canonical.mcp, servers: ordinaryServers, gateway: undefined, hub: undefined, routes: directRoutes, runtime: undefined };
|
|
70
72
|
// The managed set is passed through unchanged, so an unmanaged agent
|
|
71
73
|
// reaches nothing. Being spawned is NOT treated as consent here, which
|
|
@@ -80,7 +82,7 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
80
82
|
// user just said to stop involving.
|
|
81
83
|
const { desired, conflicts } = resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
82
84
|
return {
|
|
83
|
-
upstreams: desired.map(({ name
|
|
85
|
+
upstreams: desired.map(({ name }) => ({ name, def: canonical.mcp.servers[name] })),
|
|
84
86
|
conflicts: conflicts.map((conflict) => conflict.message),
|
|
85
87
|
};
|
|
86
88
|
}
|
|
@@ -123,9 +125,7 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
|
123
125
|
homeDir,
|
|
124
126
|
});
|
|
125
127
|
const registry = new BuiltinRegistry([new SkillProvider(), new RuntimeMemoryProvider(), new RuntimeControlProvider(backend), new McpStatusProvider(backend), new TaskProvider(), new AgentBridgeProvider(), new UpstreamProvider(backend)]);
|
|
126
|
-
const server = createRuntimeServer(serverInfo, { agentId: opts.agentId, homeDir }, registry);
|
|
127
|
-
const transport = new StdioServerTransport();
|
|
128
|
-
await server.connect(transport);
|
|
128
|
+
const server = serveStdio(() => createRuntimeServer(serverInfo, { agentId: opts.agentId, homeDir }, registry));
|
|
129
129
|
await waitForShutdown(registry, server);
|
|
130
130
|
return { exitCode: 0 };
|
|
131
131
|
}
|