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.
Files changed (45) hide show
  1. package/dist/adapters/mcpPlan.d.ts +1 -0
  2. package/dist/adapters/mcpPlan.js +14 -5
  3. package/dist/cli.js +30 -5
  4. package/dist/commands/mcp.d.ts +71 -0
  5. package/dist/commands/mcp.js +145 -4
  6. package/dist/commands/mcpAuth.d.ts +6 -0
  7. package/dist/commands/mcpAuth.js +38 -10
  8. package/dist/commands/mcpGateway.js +7 -7
  9. package/dist/commands/skill.d.ts +45 -1
  10. package/dist/commands/skill.js +83 -2
  11. package/dist/core/canonical.d.ts +1 -0
  12. package/dist/core/canonical.js +8 -3
  13. package/dist/core/types.d.ts +4 -1
  14. package/dist/core/types.js +3 -0
  15. package/dist/lib/agentBridgeProvider.d.ts +1 -1
  16. package/dist/lib/gatewayBackend.d.ts +11 -0
  17. package/dist/lib/gatewayBackend.js +23 -4
  18. package/dist/lib/mcpConnect.d.ts +6 -3
  19. package/dist/lib/mcpConnect.js +8 -6
  20. package/dist/lib/mcpRuntime.d.ts +12 -2
  21. package/dist/lib/mcpRuntime.js +70 -9
  22. package/dist/lib/mcpStatusProvider.d.ts +1 -1
  23. package/dist/lib/memoryProvider.d.ts +1 -1
  24. package/dist/lib/oauth/discovery.d.ts +1 -0
  25. package/dist/lib/oauth/discovery.js +2 -0
  26. package/dist/lib/oauth/flow.d.ts +4 -0
  27. package/dist/lib/oauth/flow.js +48 -11
  28. package/dist/lib/oauth/refresh.d.ts +1 -0
  29. package/dist/lib/oauth/refresh.js +3 -1
  30. package/dist/lib/oauth/store.d.ts +2 -0
  31. package/dist/lib/runtimeControlProvider.d.ts +1 -1
  32. package/dist/lib/scopeSelection.d.ts +36 -0
  33. package/dist/lib/scopeSelection.js +65 -0
  34. package/dist/lib/skillManifest.d.ts +66 -0
  35. package/dist/lib/skillManifest.js +203 -0
  36. package/dist/lib/skillProvider.d.ts +21 -1
  37. package/dist/lib/skillProvider.js +61 -1
  38. package/dist/lib/taskProvider.d.ts +1 -1
  39. package/dist/lib/upstreamResources.d.ts +23 -0
  40. package/dist/lib/upstreamResources.js +192 -0
  41. package/dist/pi-bridge/bundle.js +37906 -19932
  42. package/docs/architecture.md +14 -13
  43. package/docs/mcp-authorization-ownership.md +59 -0
  44. package/docs/skills-over-mcp.md +91 -0
  45. package/package.json +6 -3
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `trellis skill list|add|remove` — command-line CRUD for canonical
2
+ * `trellis skill list|add|remove|scope` — command-line CRUD for canonical
3
3
  * skills (trellis-canonical-cli-crud), an alternative to hand-editing
4
4
  * `~/.trellis/skills/<name>/SKILL.md` directly. `add`'s conflict
5
5
  * decision and `remove`'s "sync will un-sync it" behavior both reuse
@@ -10,6 +10,7 @@
10
10
  import type { AgentId } from "../core/types.js";
11
11
  import { type BackupSession } from "../lib/backup.js";
12
12
  import type { SyncReport } from "./sync.js";
13
+ import { type ScopeSelectorRaw } from "../lib/scopeSelection.js";
13
14
  import type { RemoteSkillLockEntry } from "../lib/remoteSkillLock.js";
14
15
  export type BuiltinSkillUpdateAction = "create" | "update" | "already-current";
15
16
  export interface BuiltinSkillUpdatePlan {
@@ -97,3 +98,46 @@ export declare function runSkillRemove(name: string, opts?: {
97
98
  }): Promise<{
98
99
  exitCode: number;
99
100
  }>;
101
+ export type SkillScopeAction = "updated" | "already-set" | "not-found" | "invalid-input";
102
+ export interface SkillScopePlan {
103
+ name: string;
104
+ action: SkillScopeAction;
105
+ detail: string;
106
+ /** What will be recorded in `scope.yaml`; `undefined` clears the explicit
107
+ * scope so the skill reaches every managed agent. Set when the selection was
108
+ * valid, whatever the action. */
109
+ scope?: AgentId[];
110
+ mode?: "all" | "none" | "explicit";
111
+ /** True when a full selection was dropped instead of recorded
112
+ * (trellis-scope-editing-and-auth-status design.md D3). */
113
+ normalizedFromFull?: boolean;
114
+ /** The agents that will actually receive the skill afterwards. */
115
+ effective?: readonly AgentId[];
116
+ }
117
+ /**
118
+ * `skill scope <name> --agents a,b | --all | --none`
119
+ * (trellis-scope-editing-and-auth-status tasks.md 1.1). Plan only — the write
120
+ * and the cascade sync live in `applySkillScopeWithSync`.
121
+ */
122
+ export declare function collectSkillScopePlan(name: string | undefined, raw: ScopeSelectorRaw, homeDir?: string): SkillScopePlan;
123
+ export interface SkillScopeOutcome {
124
+ plan: SkillScopePlan;
125
+ writeError?: string;
126
+ sync?: SyncReport;
127
+ }
128
+ /** Write inside a backup session so `trellis rollback` covers it, then the
129
+ * same skills sync `add` / `remove` run — narrowing is converged by
130
+ * `symlinkPlan`'s existing "canonical entry gone or scoped away" removal
131
+ * (design.md D5). */
132
+ export declare function applySkillScopeWithSync(plan: SkillScopePlan, opts?: {
133
+ homeDir?: string;
134
+ dryRun?: boolean;
135
+ backupSession?: BackupSession;
136
+ }): Promise<SkillScopeOutcome>;
137
+ export declare function runSkillScope(name: string | undefined, raw: ScopeSelectorRaw, opts?: {
138
+ homeDir?: string;
139
+ json?: boolean;
140
+ dryRun?: boolean;
141
+ }): Promise<{
142
+ exitCode: number;
143
+ }>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `trellis skill list|add|remove` — command-line CRUD for canonical
2
+ * `trellis skill list|add|remove|scope` — command-line CRUD for canonical
3
3
  * skills (trellis-canonical-cli-crud), an alternative to hand-editing
4
4
  * `~/.trellis/skills/<name>/SKILL.md` directly. `add`'s conflict
5
5
  * decision and `remove`'s "sync will un-sync it" behavior both reuse
@@ -14,12 +14,13 @@ import { dirname } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { decideDirImport } from "../lib/dirEquals.js";
16
16
  import { findSkillFile } from "../lib/skillFile.js";
17
- import { loadCanonicalSource } from "../core/canonical.js";
17
+ import { loadCanonicalSource, writeSkillScopeYaml } from "../core/canonical.js";
18
18
  import { resolveScope } from "../core/types.js";
19
19
  import { isBuiltinSkillName } from "../lib/builtinSkills.js";
20
20
  import { openBackupSession } from "../lib/backup.js";
21
21
  import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
22
22
  import { remoteSkillProvenance } from "./remoteSkill.js";
23
+ import { describeScope, resolveScopeSelection, sameScope } from "../lib/scopeSelection.js";
23
24
  function builtinSkillTemplate() {
24
25
  const here = dirname(fileURLToPath(import.meta.url));
25
26
  return readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
@@ -205,3 +206,83 @@ export async function runSkillRemove(name, opts = {}) {
205
206
  const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
206
207
  return { exitCode: failed || syncConflict ? 1 : 0 };
207
208
  }
209
+ /**
210
+ * `skill scope <name> --agents a,b | --all | --none`
211
+ * (trellis-scope-editing-and-auth-status tasks.md 1.1). Plan only — the write
212
+ * and the cascade sync live in `applySkillScopeWithSync`.
213
+ */
214
+ export function collectSkillScopePlan(name, raw, homeDir = homedir()) {
215
+ if (!name)
216
+ return { name: "(none)", action: "invalid-input", detail: "usage: trellis skill scope <name> --agents <ids> | --all | --none" };
217
+ if (isBuiltinSkillName(name)) {
218
+ return { name, action: "invalid-input", detail: `"${name}" is a Trellis package-owned Skill — its scope is not editable` };
219
+ }
220
+ const canonical = loadCanonicalSource(homeDir);
221
+ const selection = resolveScopeSelection(raw, canonical.managedAgents);
222
+ if (!selection.ok)
223
+ return { name, action: "invalid-input", detail: selection.detail };
224
+ const skill = canonical.skills.find((candidate) => candidate.name === name);
225
+ if (!skill)
226
+ return { name, action: "not-found", detail: `no canonical skill named "${name}"` };
227
+ const base = {
228
+ name,
229
+ scope: selection.scope,
230
+ mode: selection.mode,
231
+ normalizedFromFull: selection.normalizedFromFull,
232
+ effective: resolveScope(selection.scope, canonical.managedAgents),
233
+ };
234
+ const summary = describeScope(selection.scope, canonical.managedAgents);
235
+ const normalized = selection.normalizedFromFull ? " (every managed agent selected — recorded as the default, so a newly managed agent is included)" : "";
236
+ if (sameScope(skill.scope, selection.scope)) {
237
+ return { ...base, action: "already-set", detail: `scope is already ${summary}` };
238
+ }
239
+ return { ...base, action: "updated", detail: `set scope to ${summary}${normalized}` };
240
+ }
241
+ /** Write inside a backup session so `trellis rollback` covers it, then the
242
+ * same skills sync `add` / `remove` run — narrowing is converged by
243
+ * `symlinkPlan`'s existing "canonical entry gone or scoped away" removal
244
+ * (design.md D5). */
245
+ export async function applySkillScopeWithSync(plan, opts = {}) {
246
+ const homeDir = opts.homeDir ?? homedir();
247
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "updated" ? openBackupSession(homeDir, "skill-scope") : undefined;
248
+ const session = opts.backupSession ?? ownSession;
249
+ let writeError;
250
+ if (!opts.dryRun && plan.action === "updated") {
251
+ const result = writeSkillScopeYaml(join(homeDir, ".trellis", "scope.yaml"), plan.name, plan.scope, session);
252
+ if (!result.ok)
253
+ writeError = result.error;
254
+ }
255
+ ownSession?.finalize();
256
+ let syncReport;
257
+ if (!writeError && plan.action === "updated" && existsSync(join(homeDir, ".trellis"))) {
258
+ syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
259
+ }
260
+ return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
261
+ }
262
+ export async function runSkillScope(name, raw, opts = {}) {
263
+ const homeDir = opts.homeDir ?? homedir();
264
+ let outcome;
265
+ try {
266
+ outcome = await applySkillScopeWithSync(collectSkillScopePlan(name, raw, homeDir), { homeDir, dryRun: opts.dryRun });
267
+ }
268
+ catch (err) {
269
+ console.error(err instanceof Error ? err.message : String(err));
270
+ return { exitCode: 1 };
271
+ }
272
+ const { plan, writeError, sync: syncReport } = outcome;
273
+ if (opts.json) {
274
+ const payload = writeError ? { ...plan, writeError } : plan;
275
+ console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
276
+ }
277
+ else {
278
+ console.log(`${opts.dryRun ? "[dry run] " : ""}skill scope ${plan.name}`);
279
+ console.log(` [${plan.action}] ${plan.detail}`);
280
+ if (writeError)
281
+ console.error(` write failed: ${writeError}`);
282
+ if (syncReport)
283
+ printSyncReport(syncReport, opts.dryRun ?? false);
284
+ }
285
+ const failed = plan.action === "invalid-input" || plan.action === "not-found" || Boolean(writeError);
286
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
287
+ return { exitCode: failed || syncConflict ? 1 : 0 };
288
+ }
@@ -25,6 +25,7 @@ type McpServerDefYaml = Omit<McpServerDef, "staticEnv" | "envAliases" | "auth">
25
25
  * them — a YAML cast is optimistic, validation is this module's job. */
26
26
  type McpAuthYaml = string | {
27
27
  kind?: unknown;
28
+ owner?: unknown;
28
29
  client_id?: unknown;
29
30
  client_secret_env?: unknown;
30
31
  };
@@ -25,7 +25,7 @@ function fromServerDefYaml(serverName, def) {
25
25
  * `auth` translation and shape validation
26
26
  * (trellis-mcp-oauth-static-client tasks 1.1/1.3): the scalar and the
27
27
  * object form share one discriminator — `kind: oauth` — and the object
28
- * accepts exactly `kind`, `client_id`, `client_secret_env`. Everything
28
+ * accepts exactly `kind`, `owner`, `client_id`, `client_secret_env`. Everything
29
29
  * else is refused here at load, naming the field but never echoing the
30
30
  * value: an invalid `client_secret_env` is at best a typo, at worst a
31
31
  * pasted secret.
@@ -36,10 +36,10 @@ function fromAuthYaml(serverName, auth) {
36
36
  if (typeof auth !== "object" || auth === null) {
37
37
  throw new Error(`mcp/servers.yaml: "${serverName}" auth must be the scalar "oauth" or { kind: oauth, client_id?, client_secret_env? }`);
38
38
  }
39
- const { kind, client_id, client_secret_env, ...extra } = auth;
39
+ const { kind, owner, client_id, client_secret_env, ...extra } = auth;
40
40
  const unknownKey = Object.keys(extra)[0];
41
41
  if (unknownKey !== undefined) {
42
- throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" — expected kind, client_id, client_secret_env`);
42
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" — expected kind, owner, client_id, client_secret_env`);
43
43
  }
44
44
  if (kind !== "oauth") {
45
45
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.kind must be "oauth" — kind is the only recognized discriminator`);
@@ -51,6 +51,10 @@ function fromAuthYaml(serverName, auth) {
51
51
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.client_secret_env must be a variable NAME (e.g. FIGMA_CLIENT_SECRET), never a secret value`);
52
52
  }
53
53
  const out = { kind: "oauth" };
54
+ if (owner !== undefined && owner !== "agent" && owner !== "trellis")
55
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth.owner must be agent or trellis`);
56
+ if (owner !== undefined)
57
+ out.owner = owner;
54
58
  if (typeof client_id === "string")
55
59
  out.clientId = client_id;
56
60
  if (typeof client_secret_env === "string")
@@ -73,6 +77,7 @@ export function toServerDefYaml(def) {
73
77
  ? auth
74
78
  : {
75
79
  kind: auth.kind,
80
+ ...(auth.owner !== undefined ? { owner: auth.owner } : {}),
76
81
  ...(auth.clientId !== undefined ? { client_id: auth.clientId } : {}),
77
82
  ...(auth.clientSecretEnv !== undefined ? { client_secret_env: auth.clientSecretEnv } : {}),
78
83
  };
@@ -15,6 +15,8 @@ export type McpAuthMode = "oauth";
15
15
  */
16
16
  export interface McpAuthConfig {
17
17
  kind: McpAuthMode;
18
+ /** Omitted preserves each Agent's native authorization. */
19
+ owner?: "agent" | "trellis";
18
20
  clientId?: string;
19
21
  clientSecretEnv?: string;
20
22
  }
@@ -23,6 +25,7 @@ export interface McpAuthConfig {
23
25
  * object form exists — a strict-equality check would silently strip the
24
26
  * OAuth routing from metadata-bearing servers. */
25
27
  export declare function isOAuthAuth(auth: McpAuthMode | McpAuthConfig | undefined): boolean;
28
+ export declare function oauthOwner(auth: McpAuthMode | McpAuthConfig | undefined): "agent" | "trellis";
26
29
  /** The pre-registered client metadata a classification carries. A scalar
27
30
  * carries none — both forms route identically (design.md D1). */
28
31
  export declare function oauthClientMetadata(auth: McpAuthMode | McpAuthConfig | undefined): {
@@ -53,7 +56,7 @@ export interface McpServerDef {
53
56
  /** Explicit authorization classification. Absence means ordinary MCP;
54
57
  * Trellis never infers OAuth from a URL or a transient 401. The scalar
55
58
  * form and a metadata-less object are equivalent — metadata extends the
56
- * authorization flow, it never changes routing
59
+ * authorization flow; only explicit `owner: trellis` changes routing
57
60
  * (trellis-mcp-oauth-static-client design.md D1). */
58
61
  auth?: McpAuthMode | McpAuthConfig;
59
62
  /** stdio only */
@@ -11,6 +11,9 @@
11
11
  export function isOAuthAuth(auth) {
12
12
  return auth === "oauth" || (typeof auth === "object" && auth !== null && auth.kind === "oauth");
13
13
  }
14
+ export function oauthOwner(auth) {
15
+ return typeof auth === "object" && auth?.owner === "trellis" ? "trellis" : "agent";
16
+ }
14
17
  /** The pre-registered client metadata a classification carries. A scalar
15
18
  * carries none — both forms route identically (design.md D1). */
16
19
  export function oauthClientMetadata(auth) {
@@ -1,4 +1,4 @@
1
- import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
1
+ import type { CallToolResult, Tool } from "@modelcontextprotocol/server";
2
2
  import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
3
3
  /** Exposes every `agent-bridge.yaml` target — other than the calling
4
4
  * Agent itself — as one MCP tool per target Agent. Routing which target
@@ -18,6 +18,8 @@
18
18
  import type { McpServerDef, SecretsPolicy } from "../core/types.js";
19
19
  import { type McpClientInfo } from "./mcpConnect.js";
20
20
  import { type AggregatedTool } from "./mcpToolRegistry.js";
21
+ import type { Resource, ReadResourceResult } from "@modelcontextprotocol/client";
22
+ import type { SkillEntry } from "./skillManifest.js";
21
23
  export interface GatewayBackend {
22
24
  /** Every tool available through this backend, under its aggregated name. */
23
25
  listTools(): Promise<AggregatedTool[]>;
@@ -25,6 +27,10 @@ export interface GatewayBackend {
25
27
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
26
28
  /** Sanitized connection state for user/Agent remediation. */
27
29
  listStatus?(): readonly UpstreamStatus[];
30
+ listResources?(): Promise<Resource[]>;
31
+ readResource?(uri: string): Promise<ReadResourceResult>;
32
+ listSkills?(): Promise<SkillEntry[]>;
33
+ getSkill?(uri: string): Promise<SkillEntry | undefined>;
28
34
  /** Release everything this backend holds. Must be safe to call twice. */
29
35
  close(): Promise<void>;
30
36
  }
@@ -79,11 +85,16 @@ export declare class LocalBackend implements GatewayBackend {
79
85
  private readonly connected;
80
86
  private readonly statuses;
81
87
  private closed;
88
+ private readonly resources;
82
89
  private constructor();
83
90
  static connect(upstreams: readonly UpstreamSpec[], options: LocalBackendOptions): Promise<LocalBackend>;
84
91
  private addUpstream;
85
92
  listTools(): Promise<AggregatedTool[]>;
86
93
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
94
+ listResources(): Promise<Resource[]>;
95
+ readResource(uri: string): Promise<ReadResourceResult>;
96
+ listSkills(): Promise<SkillEntry[]>;
97
+ getSkill(uri: string): Promise<SkillEntry | undefined>;
87
98
  listStatus(): readonly UpstreamStatus[];
88
99
  close(): Promise<void>;
89
100
  private closeUpstream;
@@ -15,10 +15,13 @@
15
15
  * standard `Client` rather than something with its own shape to maintain
16
16
  * (D12).
17
17
  */
18
+ import { isOAuthAuth, oauthOwner } from "../core/types.js";
18
19
  import { connectServer } from "./mcpConnect.js";
19
20
  import { withTimeout } from "./mcpConnect.js";
20
21
  import { ensureFreshToken } from "./oauth/refresh.js";
22
+ import { readToken } from "./oauth/store.js";
21
23
  import { McpToolRegistry } from "./mcpToolRegistry.js";
24
+ import { UpstreamResources } from "./upstreamResources.js";
22
25
  /**
23
26
  * v1's backend: connects each upstream itself and aggregates the result.
24
27
  *
@@ -34,12 +37,14 @@ export class LocalBackend {
34
37
  connected = new Set();
35
38
  statuses = new Map();
36
39
  closed = false;
37
- constructor(warn) {
40
+ resources;
41
+ constructor(warn, timeoutMs) {
38
42
  this.warn = warn;
43
+ this.resources = new UpstreamResources(timeoutMs, warn);
39
44
  }
40
45
  static async connect(upstreams, options) {
41
46
  const warn = options.onWarning ?? ((message) => console.error(message));
42
- const backend = new LocalBackend(warn);
47
+ const backend = new LocalBackend(warn, options.connectTimeoutMs);
43
48
  await Promise.all(upstreams.map((upstream) => backend.addUpstream(upstream, options)));
44
49
  return backend;
45
50
  }
@@ -62,7 +67,7 @@ export class LocalBackend {
62
67
  }
63
68
  let client;
64
69
  try {
65
- client = (await connectServer(effectiveDef, options.secretsPolicy, options.connectTimeoutMs, options.clientInfo));
70
+ client = await connectServer(effectiveDef, options.secretsPolicy, options.connectTimeoutMs, options.clientInfo);
66
71
  }
67
72
  catch (err) {
68
73
  this.warn(`trellis-mcp-gateway: failed to connect to MCP server "${name}": ${errorText(err)}`);
@@ -72,7 +77,9 @@ export class LocalBackend {
72
77
  this.connected.add(client);
73
78
  let tools;
74
79
  try {
75
- const result = await withTimeout(client.listTools(), options.connectTimeoutMs, `listTools timed out after ${options.connectTimeoutMs}ms`);
80
+ const result = client.getServerCapabilities()?.tools
81
+ ? await withTimeout(client.listTools(), options.connectTimeoutMs, `listTools timed out after ${options.connectTimeoutMs}ms`)
82
+ : { tools: [] };
76
83
  tools = result.tools;
77
84
  }
78
85
  catch (err) {
@@ -82,6 +89,7 @@ export class LocalBackend {
82
89
  return;
83
90
  }
84
91
  const { skipped } = this.registry.add(name, client, tools);
92
+ this.resources.add(name, client);
85
93
  for (const collided of skipped) {
86
94
  this.warn(`trellis-mcp-gateway: tool "${collided}" from server "${name}" collides with an already-registered tool and was skipped`);
87
95
  }
@@ -107,6 +115,10 @@ export class LocalBackend {
107
115
  async callTool(name, args) {
108
116
  return this.registry.callTool(name, args);
109
117
  }
118
+ listResources() { return this.resources.listResources(); }
119
+ readResource(uri) { return this.resources.readResource(uri); }
120
+ listSkills() { return this.resources.listSkills(); }
121
+ getSkill(uri) { return this.resources.getSkill(uri); }
110
122
  listStatus() {
111
123
  return [...this.statuses.values()];
112
124
  }
@@ -189,9 +201,16 @@ function failureStatus(name, def, error) {
189
201
  * the kind of invisible precedence that is impossible to debug.
190
202
  */
191
203
  export async function withOAuthHeader(name, def, homeDir) {
204
+ if (!isOAuthAuth(def.auth) || oauthOwner(def.auth) !== "trellis")
205
+ return def;
192
206
  const hasExplicitAuth = Object.keys(def.headers ?? {}).some((key) => key.toLowerCase() === "authorization");
193
207
  if (hasExplicitAuth)
194
208
  return def;
209
+ const stored = readToken(homeDir, name);
210
+ const resource = new URL(def.url);
211
+ resource.hash = "";
212
+ if (stored?.resourceUrl && stored.resourceUrl !== resource.toString())
213
+ throw new Error("OAuth authorization required: saved credential targets another MCP resource");
195
214
  const token = await ensureFreshToken(homeDir, name);
196
215
  if (!token)
197
216
  return def;
@@ -12,8 +12,7 @@
12
12
  * which servers are in scope, or knows what a tool is; callers isolate
13
13
  * failures in whatever way suits them.
14
14
  */
15
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
- import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
15
+ import { Client } from "@modelcontextprotocol/client";
17
16
  import type { McpServerDef, SecretsPolicy } from "../core/types.js";
18
17
  /** What an MCP upstream is told this client is. Each consumer passes its
19
18
  * own so an upstream's logs name the thing that actually connected. */
@@ -40,7 +39,11 @@ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, m
40
39
  * on close, but the original connect failure is what the caller needs
41
40
  * to see, not a secondary cleanup error.
42
41
  */
43
- export declare function connectWithCleanup(client: Client, transport: Transport, timeoutMs: number, message: string): Promise<Client>;
42
+ export declare function connectWithCleanup<T, TTransport extends {
43
+ close(): Promise<void>;
44
+ }>(client: T & {
45
+ connect(transport: TTransport): Promise<unknown>;
46
+ }, transport: TTransport, timeoutMs: number, message: string): Promise<T>;
44
47
  export declare function connectStdio(def: McpServerDef, secretsPolicy: SecretsPolicy, timeoutMs: number, clientInfo: McpClientInfo): Promise<Client>;
45
48
  export declare function resolveHeaders(def: McpServerDef, secretsPolicy: SecretsPolicy): Record<string, string> | undefined;
46
49
  export declare function connectHttp(def: McpServerDef, secretsPolicy: SecretsPolicy, timeoutMs: number, clientInfo: McpClientInfo): Promise<Client>;
@@ -12,10 +12,8 @@
12
12
  * which servers are in scope, or knows what a tool is; callers isolate
13
13
  * failures in whatever way suits them.
14
14
  */
15
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
- import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
17
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
18
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
15
+ import { Client, StreamableHTTPClientTransport, SSEClientTransport } from "@modelcontextprotocol/client";
16
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
19
17
  import { resolveSecretEnv } from "./secretEnv.js";
20
18
  import { extractTemplateVarNames } from "./envVarNames.js";
21
19
  export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
@@ -68,7 +66,7 @@ export async function connectWithCleanup(client, transport, timeoutMs, message)
68
66
  }
69
67
  }
70
68
  export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
71
- const client = new Client(clientInfo, { capabilities: {} });
69
+ const client = new Client(clientInfo, { capabilities: {}, versionNegotiation: { mode: "auto", probe: { timeoutMs: Math.min(3000, timeoutMs / 2) } } });
72
70
  const envAliases = def.envAliases ?? {};
73
71
  // `envAliases`' values are source variable names — resolved through the
74
72
  // exact same call as `env`'s own names, never delivered as the raw
@@ -81,6 +79,9 @@ export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
81
79
  const transport = new StdioClientTransport({
82
80
  command: def.command,
83
81
  args: def.args,
82
+ // A 16 MiB skill file can expand sixfold when JSON escapes control bytes.
83
+ // Keep framing bounded without rejecting a valid Skills-extension file.
84
+ maxBufferSize: 128 * 1024 * 1024,
84
85
  // staticEnv merges last: a literal, intentionally-plain value (an
85
86
  // email, an environment tag) always wins over an unresolved name-only
86
87
  // entry's empty-string fallback for the same key — though in practice
@@ -102,12 +103,13 @@ export function resolveHeaders(def, secretsPolicy) {
102
103
  return result;
103
104
  }
104
105
  export async function connectHttp(def, secretsPolicy, timeoutMs, clientInfo) {
105
- const client = new Client(clientInfo, { capabilities: {} });
106
+ const client = new Client(clientInfo, { capabilities: {}, versionNegotiation: { mode: "auto", probe: { timeoutMs } } });
106
107
  const headers = resolveHeaders(def, secretsPolicy);
107
108
  const opts = headers ? { requestInit: { headers } } : undefined;
108
109
  return connectWithCleanup(client, new StreamableHTTPClientTransport(new URL(def.url), opts), timeoutMs, `connect timed out after ${timeoutMs}ms`);
109
110
  }
110
111
  export async function connectSse(def, secretsPolicy, timeoutMs, clientInfo) {
112
+ // HTTP+SSE is a legacy transport; it cannot carry modern discovery.
111
113
  const client = new Client(clientInfo, { capabilities: {} });
112
114
  const headers = resolveHeaders(def, secretsPolicy);
113
115
  const opts = headers ? { requestInit: { headers } } : undefined;
@@ -1,7 +1,8 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
1
+ import { Server } from "@modelcontextprotocol/server";
2
+ import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/server";
3
3
  import type { AgentId } from "../core/types.js";
4
4
  import type { GatewayBackend } from "./gatewayBackend.js";
5
+ import type { SkillEntry } from "./skillManifest.js";
5
6
  export interface RuntimeContext {
6
7
  agentId: AgentId;
7
8
  homeDir: string;
@@ -14,6 +15,9 @@ export interface TrellisProvider {
14
15
  readResource?(uri: URL, context: RuntimeContext): Promise<ReadResourceResult> | ReadResourceResult;
15
16
  listPrompts?(context: RuntimeContext): Promise<Prompt[]> | Prompt[];
16
17
  getPrompt?(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult> | GetPromptResult;
18
+ /** Skills-over-MCP `skills/list` and `skills/get` (trellis-skills-over-mcp). */
19
+ listSkillEntries?(context: RuntimeContext): Promise<SkillEntry[]> | SkillEntry[];
20
+ getSkillEntry?(uri: string, context: RuntimeContext): Promise<SkillEntry | undefined> | SkillEntry | undefined;
17
21
  close?(): Promise<void>;
18
22
  }
19
23
  export declare class BuiltinRegistry {
@@ -29,6 +33,8 @@ export declare class BuiltinRegistry {
29
33
  callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
30
34
  listResources(context: RuntimeContext): Promise<Resource[]>;
31
35
  readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
36
+ listSkills(context: RuntimeContext): Promise<SkillEntry[]>;
37
+ getSkill(uri: string, context: RuntimeContext): Promise<SkillEntry | undefined>;
32
38
  listPrompts(context: RuntimeContext): Promise<Prompt[]>;
33
39
  getPrompt(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult>;
34
40
  close(): Promise<void>;
@@ -41,6 +47,10 @@ export declare class UpstreamProvider implements TrellisProvider {
41
47
  constructor(backend: GatewayBackend);
42
48
  listTools(): Promise<Tool[]>;
43
49
  callTool(name: string, args: unknown): Promise<CallToolResult>;
50
+ listResources(): Promise<Resource[]>;
51
+ readResource(uri: URL): Promise<ReadResourceResult>;
52
+ listSkillEntries(): Promise<SkillEntry[]>;
53
+ getSkillEntry(uri: string): Promise<SkillEntry | undefined>;
44
54
  close(): Promise<void>;
45
55
  }
46
56
  export declare function createRuntimeServer(serverInfo: Implementation, context: RuntimeContext, registry: BuiltinRegistry): Server;
@@ -1,6 +1,7 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1
+ import { Server, ProtocolErrorCode, ProtocolError } from "@modelcontextprotocol/server";
2
+ import { z } from "zod/v4";
3
3
  import { allocateExposedToolNames } from "./mcpToolRegistry.js";
4
+ import { withoutUpstreamServerInfo } from "./upstreamResources.js";
4
5
  function compactBuiltinToolName(name) {
5
6
  return name.startsWith("trellis.") ? name.slice("trellis.".length).replaceAll(".", "_") : name;
6
7
  }
@@ -102,10 +103,42 @@ export class BuiltinRegistry {
102
103
  await this.listResources(context);
103
104
  provider = this.resourceOwners.get(key);
104
105
  }
106
+ // A manifest can introduce supporting files not returned by resources/list.
107
+ if (!provider && uri.protocol === "trellis-upstream:")
108
+ provider = this.providers.find((candidate) => candidate.id === "upstream");
105
109
  if (!provider?.readResource)
106
110
  throw new Error(`unknown Trellis resource "${key}"`);
107
111
  return provider.readResource(uri, context);
108
112
  }
113
+ async listSkills(context) {
114
+ const skills = [];
115
+ for (const provider of this.providers) {
116
+ if (!provider.listSkillEntries)
117
+ continue;
118
+ try {
119
+ skills.push(...(await provider.listSkillEntries(context)));
120
+ }
121
+ catch (err) {
122
+ this.warn(providerError(provider.id, err));
123
+ }
124
+ }
125
+ return skills;
126
+ }
127
+ async getSkill(uri, context) {
128
+ for (const provider of this.providers) {
129
+ if (!provider.getSkillEntry)
130
+ continue;
131
+ try {
132
+ const entry = await provider.getSkillEntry(uri, context);
133
+ if (entry)
134
+ return entry;
135
+ }
136
+ catch (err) {
137
+ this.warn(providerError(provider.id, err));
138
+ }
139
+ }
140
+ return undefined;
141
+ }
109
142
  async listPrompts(context) {
110
143
  const prompts = [];
111
144
  this.promptOwners.clear();
@@ -161,8 +194,16 @@ export class UpstreamProvider {
161
194
  return (await this.backend.listTools());
162
195
  }
163
196
  async callTool(name, args) {
164
- return (await this.backend.callTool(name, args));
197
+ return withoutUpstreamServerInfo((await this.backend.callTool(name, args)));
198
+ }
199
+ async listResources() { return await this.backend.listResources?.() ?? []; }
200
+ async readResource(uri) {
201
+ if (!this.backend.readResource)
202
+ throw new Error("upstream resources unavailable");
203
+ return this.backend.readResource(uri.toString());
165
204
  }
205
+ async listSkillEntries() { return await this.backend.listSkills?.() ?? []; }
206
+ async getSkillEntry(uri) { return this.backend.getSkill?.(uri); }
166
207
  async close() {
167
208
  await this.backend.close();
168
209
  }
@@ -173,13 +214,33 @@ export function createRuntimeServer(serverInfo, context, registry) {
173
214
  tools: { listChanged: true },
174
215
  resources: { listChanged: true },
175
216
  prompts: { listChanged: true },
217
+ extensions: { "io.modelcontextprotocol/skills": {} },
176
218
  },
177
219
  });
178
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await registry.listTools(context) }));
179
- server.setRequestHandler(CallToolRequestSchema, async (request) => registry.callTool(request.params.name, request.params.arguments, context));
180
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await registry.listResources(context) }));
181
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => registry.readResource(new URL(request.params.uri), context));
182
- server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await registry.listPrompts(context) }));
183
- server.setRequestHandler(GetPromptRequestSchema, async (request) => registry.getPrompt(request.params.name, request.params.arguments, context));
220
+ server.setRequestHandler("tools/list", async () => ({ tools: await registry.listTools(context) }));
221
+ server.setRequestHandler("tools/call", async ({ params }) => registry.callTool(params.name, params.arguments, context));
222
+ server.setRequestHandler("resources/list", async () => ({ resources: await registry.listResources(context) }));
223
+ server.setRequestHandler("resources/read", async ({ params }) => {
224
+ try {
225
+ return await registry.readResource(new URL(params.uri), context);
226
+ }
227
+ catch (err) {
228
+ if (err instanceof TypeError || (err instanceof Error && /unknown|not found|not allowed|outside|secret-like|invalid.*uri/i.test(err.message))) {
229
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, "unknown or unreadable resource");
230
+ }
231
+ throw err;
232
+ }
233
+ });
234
+ server.setRequestHandler("prompts/list", async () => ({ prompts: await registry.listPrompts(context) }));
235
+ server.setRequestHandler("prompts/get", async ({ params }) => registry.getPrompt(params.name, params.arguments, context));
236
+ const result = z.looseObject({});
237
+ const cache = { ttlMs: 0, cacheScope: "private" };
238
+ server.setRequestHandler("skills/list", { params: z.object({ cursor: z.string().optional() }), result }, async () => ({ ...cache, skills: await registry.listSkills(context) }));
239
+ server.setRequestHandler("skills/get", { params: z.object({ uri: z.string() }), result }, async ({ uri }) => {
240
+ const entry = await registry.getSkill(uri, context);
241
+ if (!entry)
242
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, "unknown skill");
243
+ return { ...cache, skill: entry };
244
+ });
184
245
  return server;
185
246
  }
@@ -1,4 +1,4 @@
1
- import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
1
+ import type { CallToolResult, Tool } from "@modelcontextprotocol/server";
2
2
  import type { GatewayBackend } from "./gatewayBackend.js";
3
3
  import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
4
4
  export declare class McpStatusProvider implements TrellisProvider {
@@ -1,4 +1,4 @@
1
- import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
1
+ import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/server";
2
2
  import type { Scope } from "../core/types.js";
3
3
  import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
4
4
  /**
@@ -27,6 +27,7 @@ export interface AuthorizationServerMetadata {
27
27
  registrationEndpoint?: string;
28
28
  scopesSupported?: string[];
29
29
  codeChallengeMethodsSupported?: string[];
30
+ issuerParameterSupported?: boolean;
30
31
  }
31
32
  export type FetchLike = (url: string, init?: {
32
33
  method?: string;
@@ -37,6 +37,8 @@ function toMetadata(raw) {
37
37
  out.scopesSupported = raw.scopes_supported;
38
38
  if (raw.code_challenge_methods_supported)
39
39
  out.codeChallengeMethodsSupported = raw.code_challenge_methods_supported;
40
+ if (raw.authorization_response_iss_parameter_supported)
41
+ out.issuerParameterSupported = true;
40
42
  return out;
41
43
  }
42
44
  async function tryJson(fetchImpl, url) {