loopengine 0.1.97 → 0.1.99

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 (35) hide show
  1. package/dist/bin/ability-manager.d.ts +35 -2
  2. package/dist/bin/ability-manager.js +168 -35
  3. package/dist/core/ability-details.js +11 -0
  4. package/dist/core/mcp-abilities.d.ts +91 -0
  5. package/dist/core/mcp-abilities.js +400 -0
  6. package/dist/core/mcp-ability-generator.d.ts +59 -0
  7. package/dist/core/mcp-ability-generator.js +193 -0
  8. package/dist/core/mcp-apps.d.ts +95 -0
  9. package/dist/core/mcp-apps.js +254 -0
  10. package/dist/core/mcp-directory.d.ts +32 -0
  11. package/dist/core/mcp-directory.js +191 -0
  12. package/dist/core/run-agent.js +5 -2
  13. package/dist/core/system-agents/interviewer.d.ts +1 -1
  14. package/dist/core/system-agents/interviewer.js +1 -1
  15. package/dist/core/system-agents/templates.d.ts +11 -0
  16. package/dist/core/system-agents/templates.js +22 -16
  17. package/dist/core/system-agents/workflow-planner.d.ts +36 -3
  18. package/dist/core/system-agents/workflow-planner.js +24 -10
  19. package/dist/core/workspace-abilities.js +1 -1
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/web/abilities-admin.d.ts +1 -1
  23. package/dist/web/abilities-admin.js +1 -1
  24. package/dist/web/agents-config-page.js +187 -11
  25. package/dist/web/console-shell.js +7 -5
  26. package/dist/web/dev-ui-styles.js +1 -1
  27. package/dist/web/global-config-page.js +293 -13
  28. package/dist/web/interview-admin.js +2 -0
  29. package/dist/web/templates-admin.d.ts +10 -2
  30. package/dist/web/templates-admin.js +36 -3
  31. package/dist/web/templates-page.js +1 -0
  32. package/dist/web/workflow-admin.d.ts +46 -0
  33. package/dist/web/workflow-admin.js +195 -3
  34. package/dist/web/workflow-page.js +33 -3
  35. package/package.json +10 -1
@@ -1,4 +1,5 @@
1
1
  import type { EnvScope } from '../core/agent-env.js';
2
+ import { type InstalledMcpRecord } from '../core/mcp-abilities.js';
2
3
  export declare class AbilityManifestError extends Error {
3
4
  }
4
5
  export declare class AbilityVersionError extends Error {
@@ -55,6 +56,9 @@ export interface AbilityManifest {
55
56
  version: string;
56
57
  loopengineVersion: string;
57
58
  tools?: string[];
59
+ /** Tools served by an MCP server instead of tool files — see
60
+ * core/mcp-abilities.ts's McpServerManifest. */
61
+ mcp?: unknown;
58
62
  skills?: string[];
59
63
  actauth?: string;
60
64
  env?: AbilityEnvDecl[];
@@ -86,6 +90,10 @@ export interface InstalledAbilityRecord {
86
90
  * has no recorded value — see oldContentSpec's own fallback. */
87
91
  spec?: string;
88
92
  tools: string[];
93
+ /** An MCP ability's server and tools (core/mcp-abilities.ts loads
94
+ * them at run time). Its tools have no files, so they aren't in
95
+ * `tools`. */
96
+ mcp?: InstalledMcpRecord;
89
97
  skills: string[];
90
98
  actauthRules: string[];
91
99
  env: AbilityEnvDecl[];
@@ -178,13 +186,24 @@ export interface FetchOptions {
178
186
  *
179
187
  * All-or-nothing: every collision check below runs, and the whole
180
188
  * install is refused, before a single file is written. */
181
- export declare function installAbility(agentName: string, spec: string, options?: FetchOptions): Promise<{
189
+ /** For an ability that takes its tools from an MCP server: which of its
190
+ * server tools this agent gets (by the server's names), and each one's
191
+ * permission when it shouldn't be the ability's default. An app's ability
192
+ * (made from its server, listing every tool) given no choice gets just
193
+ * its read-only tools, those it allows by default. */
194
+ export interface McpToolChoice {
195
+ mcpTools?: string[];
196
+ mcpDecisions?: Record<string, 'allow' | 'ask' | 'deny'>;
197
+ }
198
+ export declare function installAbility(agentName: string, spec: string, options?: FetchOptions & McpToolChoice): Promise<{
182
199
  installed: string[];
183
200
  dependencies: string[];
184
201
  }>;
185
202
  export interface UpgradeFileResult {
186
203
  path: string;
187
- status: 'updated' | 'unchanged' | 'conflict';
204
+ /** added/removed: a rule the new version adds, or a tool file it no
205
+ * longer has (e.g. once its tools come from an MCP server). */
206
+ status: 'updated' | 'unchanged' | 'conflict' | 'added' | 'removed';
188
207
  }
189
208
  /** Upgrades an already-installed ability to whatever `spec` (defaulting
190
209
  * to `abilityName`, i.e. "latest") currently resolves to. Tool and skill
@@ -203,6 +222,20 @@ export declare function upgradeAbility(agentName: string, abilityName: string, o
203
222
  }): Promise<{
204
223
  files: UpgradeFileResult[];
205
224
  }>;
225
+ /** Sets which of an MCP ability's server tools an agent has, and each
226
+ * one's permission for that agent. `abilityDir` is the ability's folder
227
+ * (an app's workspace copy), which lists every tool. A tool's permission
228
+ * is its first rule in the agent's actauth.yml (an agent's own rule for a
229
+ * tool is changed rather than shadowed); a tool with no rule gets the
230
+ * ability's. Rules the ability added for tools taken away go with them. */
231
+ export declare function configureAgentMcpTools(agentName: string, abilityName: string, abilityDir: string, choice: {
232
+ remote: string;
233
+ decision: 'allow' | 'ask' | 'deny';
234
+ }[]): {
235
+ added: string[];
236
+ removed: string[];
237
+ changed: string[];
238
+ };
206
239
  /** Removes an installed ability's tool and skill files, and its actauth
207
240
  * rules. A file whose content no longer matches the hash recorded at
208
241
  * install/last-upgrade is refused (reported in `refused`, left on disk)
@@ -26,6 +26,8 @@ import { parse as parseYaml } from 'yaml';
26
26
  import semver from 'semver';
27
27
  import { agentDir } from '../core/gateway-tools.js';
28
28
  import { abilityFetchAuth } from '../core/ability-sources.js';
29
+ import { clearMcpSchemaCache, resolveMcpManifest, McpManifestError } from '../core/mcp-abilities.js';
30
+ import { appRuleName } from '../core/mcp-ability-generator.js';
29
31
  import { addToolToIndex, removeToolFromIndex, toCamelCase } from '../web/http-tool-admin.js';
30
32
  import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
31
33
  export class AbilityManifestError extends Error {
@@ -350,36 +352,16 @@ function parseActauthRules(abilityDir, manifest) {
350
352
  return parsed;
351
353
  }
352
354
  // ---- Install ----
353
- /** Installs `spec` (anything `npm pack` accepts) for `agentName`:
354
- * writes each manifest-listed tool file and patches tools/index.ts
355
- * (reusing addToolToIndex exactly as web/http-tool-admin.ts's own
356
- * createHttpTool does), copies each skill directory verbatim (a raw
357
- * cpSync, not skills-admin.ts's writeSkill — that regenerates
358
- * frontmatter from scratch and can't carry an ability's own
359
- * frontmatter/assets), and appends each actauth rule (addActauthRule).
360
- *
361
- * Deliberately does *not* attempt a live in-memory registry splice the
362
- * way adapters/http.ts's own handleHttpToolPost does for an
363
- * admin-created tool — that works there because the HTTP request
364
- * creating the tool runs *inside the same process* as the already-running
365
- * server, so updateAgent's mutation is visible to every later request in
366
- * that same process immediately. `add-ability` is a separate, one-off
367
- * CLI process with no connection to whatever server might be running
368
- * elsewhere — even a successful getEntry/updateAgent call here would
369
- * only mutate *this* CLI invocation's own throwaway registry, then
370
- * vanish the moment the process exits, having done nothing to the real
371
- * running server. (Confirmed live: this used to import
372
- * core/agent-registry.js, whose own discoverAgents does a top-level
373
- * directory scan relative to *its own compiled file's location* —
374
- * inside node_modules/loopengine when this runs there, not the
375
- * consuming project's real agents/ at all, throwing ENOENT immediately.)
376
- * Same reason add-agent/add-subagent above don't attempt this either —
377
- * a new tool becomes active the same way a new agent does: the next
378
- * request under `serve`, or automatically under `npx loopengine dev`'s
379
- * file watcher once tools/index.ts's own edit is picked up.
380
- *
381
- * All-or-nothing: every collision check below runs, and the whole
382
- * install is refused, before a single file is written. */
355
+ function resolveMcp(manifest) {
356
+ try {
357
+ return resolveMcpManifest(manifest.name, manifest.mcp);
358
+ }
359
+ catch (err) {
360
+ if (err instanceof McpManifestError)
361
+ throw new AbilityManifestError(err.message);
362
+ throw err;
363
+ }
364
+ }
383
365
  export async function installAbility(agentName, spec, options = {}) {
384
366
  const fetch = options.fetchAbilityDir ?? fetchAbilityDir;
385
367
  const abilityDir = fetch(spec);
@@ -391,7 +373,7 @@ export async function installAbility(agentName, spec, options = {}) {
391
373
  }
392
374
  const toolFiles = manifest.tools ?? [];
393
375
  const skillDirs = manifest.skills ?? [];
394
- const rules = parseActauthRules(abilityDir, manifest);
376
+ let rules = parseActauthRules(abilityDir, manifest);
395
377
  const toolsDir = join(agentDir(agentName), 'tools');
396
378
  const skillsDirPath = join(agentDir(agentName), 'skills');
397
379
  // Collision checks — refuse the whole install before writing anything,
@@ -415,6 +397,39 @@ export async function installAbility(agentName, spec, options = {}) {
415
397
  }
416
398
  toolNames.push(installedToolName);
417
399
  }
400
+ const mcp = manifest.mcp === undefined ? undefined : resolveMcp(manifest);
401
+ if (mcp) {
402
+ const chosen = options.mcpTools ?? (manifest.generatedFrom ? mcp.tools.filter((t) => rules.some((r) => r.tool === t.name && r.decision === 'allow')).map((t) => t.remote) : undefined);
403
+ if (chosen) {
404
+ const unknown = chosen.filter((r) => !mcp.tools.some((t) => t.remote === r));
405
+ if (unknown.length)
406
+ throw new AbilityManifestError(`${manifest.name} has no tool named ${unknown.join(', ')}.`);
407
+ const dropped = new Set(mcp.tools.filter((t) => !chosen.includes(t.remote)).map((t) => t.name));
408
+ mcp.tools = mcp.tools.filter((t) => chosen.includes(t.remote));
409
+ if (!mcp.tools.length)
410
+ throw new AbilityManifestError(`Choose at least one of ${manifest.name}'s tools.`);
411
+ rules = rules.filter((r) => !dropped.has(r.tool));
412
+ }
413
+ for (const [remote, decision] of Object.entries(options.mcpDecisions ?? {})) {
414
+ const tool = mcp.tools.find((t) => t.remote === remote);
415
+ if (!tool)
416
+ continue;
417
+ const rule = rules.find((r) => r.tool === tool.name);
418
+ if (rule)
419
+ rule.decision = decision;
420
+ else
421
+ rules.push({ name: appRuleName(manifest.name, tool.name), scope: '*/*', tool: tool.name, decision });
422
+ }
423
+ // No namespacing fallback here, unlike tool files: the ability's own
424
+ // actauth rules name these tools, so a renamed tool would silently
425
+ // lose its rule.
426
+ const taken = new Set([...toolNames, ...Object.values(provenance).flatMap((r) => r.mcp?.tools.map((t) => t.name) ?? [])]);
427
+ for (const tool of mcp.tools) {
428
+ if (taken.has(tool.name) || existsSync(join(toolsDir, `${tool.name}.ts`))) {
429
+ throw new AbilityCollisionError(`Agent '${agentName}' already has a tool named '${tool.name}'.`);
430
+ }
431
+ }
432
+ }
418
433
  const skillIds = [];
419
434
  for (const skillDir of skillDirs) {
420
435
  const bareSkillId = basename(skillDir);
@@ -516,6 +531,7 @@ export async function installAbility(agentName, spec, options = {}) {
516
531
  version: manifest.version,
517
532
  spec,
518
533
  tools: toolNames,
534
+ ...(mcp ? { mcp } : {}),
519
535
  skills: skillIds,
520
536
  actauthRules: ruleNames,
521
537
  env: manifest.env ?? [],
@@ -523,8 +539,15 @@ export async function installAbility(agentName, spec, options = {}) {
523
539
  contentHashes,
524
540
  };
525
541
  writeProvenance(agentName, provenance);
542
+ if (mcp)
543
+ clearMcpSchemaCache(agentDir(agentName), manifest.name);
526
544
  return {
527
- installed: [...toolNames.map((n) => `tools/${n}.ts`), ...skillIds.map((id) => `skills/${id}/`), ...ruleNames.map((n) => `actauth:${n}`)],
545
+ installed: [
546
+ ...toolNames.map((n) => `tools/${n}.ts`),
547
+ ...(mcp?.tools.map((t) => `mcp:${t.name}`) ?? []),
548
+ ...skillIds.map((id) => `skills/${id}/`),
549
+ ...ruleNames.map((n) => `actauth:${n}`),
550
+ ],
528
551
  dependencies: manifest.dependencies ?? [],
529
552
  };
530
553
  }
@@ -619,6 +642,7 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
619
642
  checkLoopengineVersion(newManifest, options.installedLoopengineRange);
620
643
  const results = [];
621
644
  const newContentHashes = { ...record.contentHashes };
645
+ const remainingTools = [];
622
646
  for (const toolName of record.tools) {
623
647
  const relPath = `tools/${toolName}.ts`;
624
648
  // matchesInstalledToolName, not a bare basename(f, '.ts') === toolName
@@ -627,7 +651,23 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
627
651
  // off textually the way a skill id's "/" is.
628
652
  const oldFile = (oldManifest.tools ?? []).find((f) => matchesInstalledToolName(abilityName, f, toolName));
629
653
  const newFile = (newManifest.tools ?? []).find((f) => matchesInstalledToolName(abilityName, f, toolName));
630
- if (!oldFile || !newFile) {
654
+ // The new version no longer has this tool file (its tools may now come
655
+ // from an MCP server): an unedited copy goes, an edited one stays.
656
+ if (!newFile) {
657
+ const minePath = join(agentDir(agentName), relPath);
658
+ if (isDirty(minePath, record.contentHashes[relPath])) {
659
+ remainingTools.push(toolName);
660
+ results.push({ path: relPath, status: 'conflict' });
661
+ continue;
662
+ }
663
+ rmSync(minePath, { force: true });
664
+ removeToolFromIndex(join(agentDir(agentName), 'tools', 'index.ts'), toolName, toCamelCase(toolName));
665
+ delete newContentHashes[relPath];
666
+ results.push({ path: relPath, status: 'removed' });
667
+ continue;
668
+ }
669
+ remainingTools.push(toolName);
670
+ if (!oldFile) {
631
671
  results.push({ path: relPath, status: 'unchanged' });
632
672
  continue;
633
673
  }
@@ -699,8 +739,34 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
699
739
  updateActauthRule(agentName, ruleName, { scope: newRule.scope, tool: newRule.tool, decision: newRule.decision });
700
740
  results.push({ path, status: 'updated' });
701
741
  }
742
+ // Rules the new version adds (for tools it adds) are installed, unless
743
+ // the agent already has a rule for that tool.
744
+ const actauthRules = [...record.actauthRules];
745
+ for (const rule of newRules) {
746
+ if (record.actauthRules.some((name) => matchesInstalledRuleName(abilityName, rule.name, name)))
747
+ continue;
748
+ const rules = readActauthConfig(agentName).rules;
749
+ if (rules.some((r) => r.tool === rule.tool))
750
+ continue;
751
+ const taken = new Set(rules.map((r) => r.name));
752
+ const name = taken.has(rule.name) ? namespacedRuleName(abilityName, rule.name) : rule.name;
753
+ if (taken.has(name))
754
+ continue;
755
+ addActauthRule(agentName, { ...rule, name });
756
+ actauthRules.push(name);
757
+ results.push({ path: `actauth:${name}`, status: 'added' });
758
+ }
759
+ // An MCP ability's tools are data, not files, so there's nothing to
760
+ // merge: they take the new version's server and tool list as-is.
761
+ const { mcp: _oldMcp, ...rest } = record;
762
+ const newMcp = newManifest.mcp === undefined ? undefined : resolveMcp(newManifest);
763
+ for (const tool of newMcp?.tools ?? [])
764
+ results.push({ path: `mcp:${tool.name}`, status: record.mcp?.tools.some((t) => t.name === tool.name && t.remote === tool.remote) ? 'unchanged' : 'updated' });
702
765
  provenance[abilityName] = {
703
- ...record,
766
+ ...rest,
767
+ ...(newMcp ? { mcp: newMcp } : {}),
768
+ tools: remainingTools,
769
+ actauthRules,
704
770
  version: newManifest.version,
705
771
  spec,
706
772
  contentHashes: newContentHashes,
@@ -708,8 +774,69 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
708
774
  dependencies: newManifest.dependencies ?? record.dependencies,
709
775
  };
710
776
  writeProvenance(agentName, provenance);
777
+ if (record.mcp || newMcp)
778
+ clearMcpSchemaCache(agentDir(agentName), abilityName);
711
779
  return { files: results };
712
780
  }
781
+ // ---- An agent's MCP tools ----
782
+ /** Sets which of an MCP ability's server tools an agent has, and each
783
+ * one's permission for that agent. `abilityDir` is the ability's folder
784
+ * (an app's workspace copy), which lists every tool. A tool's permission
785
+ * is its first rule in the agent's actauth.yml (an agent's own rule for a
786
+ * tool is changed rather than shadowed); a tool with no rule gets the
787
+ * ability's. Rules the ability added for tools taken away go with them. */
788
+ export function configureAgentMcpTools(agentName, abilityName, abilityDir, choice) {
789
+ const provenance = readProvenance(agentName);
790
+ const record = provenance[abilityName];
791
+ if (!record?.mcp)
792
+ throw new AbilityNotInstalledError(`Ability '${abilityName}' isn't installed as an MCP ability for '${agentName}'.`);
793
+ const manifest = readManifest(abilityDir);
794
+ const all = resolveMcp(manifest);
795
+ const unknown = choice.filter((c) => !all.tools.some((t) => t.remote === c.remote)).map((c) => c.remote);
796
+ if (unknown.length)
797
+ throw new AbilityManifestError(`${abilityName} has no tool named ${unknown.join(', ')}.`);
798
+ const tools = all.tools.filter((t) => choice.some((c) => c.remote === t.remote));
799
+ if (!tools.length)
800
+ throw new AbilityManifestError(`Choose at least one of ${abilityName}'s tools, or remove it from the agent instead.`);
801
+ const before = new Set(record.mcp.tools.map((t) => t.name));
802
+ const after = new Set(tools.map((t) => t.name));
803
+ const added = [...after].filter((t) => !before.has(t));
804
+ const removed = [...before].filter((t) => !after.has(t));
805
+ const changed = [];
806
+ const ruleNames = [...record.actauthRules];
807
+ for (const name of record.actauthRules) {
808
+ const rule = readActauthConfig(agentName).rules.find((r) => r.name === name);
809
+ if (rule && removed.includes(rule.tool)) {
810
+ try {
811
+ removeActauthRule(agentName, name);
812
+ }
813
+ catch { }
814
+ ruleNames.splice(ruleNames.indexOf(name), 1);
815
+ }
816
+ }
817
+ for (const tool of tools) {
818
+ const decision = choice.find((c) => c.remote === tool.remote).decision;
819
+ const current = readActauthConfig(agentName).rules;
820
+ const rule = current.find((r) => r.tool === tool.name);
821
+ if (rule) {
822
+ if (rule.decision !== decision) {
823
+ updateActauthRule(agentName, rule.name, { scope: rule.scope, tool: rule.tool, decision });
824
+ changed.push(tool.name);
825
+ }
826
+ continue;
827
+ }
828
+ const taken = new Set(current.map((r) => r.name));
829
+ let name = appRuleName(abilityName, tool.name);
830
+ for (let i = 2; taken.has(name); i++)
831
+ name = `${appRuleName(abilityName, tool.name)}-${i}`;
832
+ addActauthRule(agentName, { name, scope: '*/*', tool: tool.name, decision });
833
+ ruleNames.push(name);
834
+ }
835
+ provenance[abilityName] = { ...record, mcp: { ...all, tools }, version: manifest.version, actauthRules: ruleNames };
836
+ writeProvenance(agentName, provenance);
837
+ clearMcpSchemaCache(agentDir(agentName), abilityName);
838
+ return { added, removed, changed };
839
+ }
713
840
  // ---- Remove ----
714
841
  function isDirty(path, recordedHash) {
715
842
  if (!existsSync(path))
@@ -792,12 +919,18 @@ export function removeAbility(agentName, abilityName, force = false) {
792
919
  // Already gone (hand-removed since install) — not a failure.
793
920
  }
794
921
  }
922
+ // An MCP ability's tools have no files to be dirty, so they always go.
923
+ for (const tool of record.mcp?.tools ?? [])
924
+ removed.push(`mcp:${tool.name}`);
795
925
  if (refused.length === 0) {
796
926
  delete provenance[abilityName];
797
927
  }
798
928
  else {
799
- provenance[abilityName] = { ...record, tools: remainingTools, skills: remainingSkills, actauthRules: [] };
929
+ const { mcp: _mcp, ...rest } = record;
930
+ provenance[abilityName] = { ...rest, tools: remainingTools, skills: remainingSkills, actauthRules: [] };
800
931
  }
801
932
  writeProvenance(agentName, provenance);
933
+ if (record.mcp)
934
+ clearMcpSchemaCache(agentDir(agentName), abilityName);
802
935
  return { removed, refused };
803
936
  }
@@ -7,6 +7,7 @@ import { existsSync, readFileSync } from 'node:fs';
7
7
  import { basename, join } from 'node:path';
8
8
  import ts from 'typescript';
9
9
  import { parse as parseYaml } from 'yaml';
10
+ import { resolveMcpManifest } from './mcp-abilities.js';
10
11
  /** Throws when `dir` holds no loopengine.ability.json. */
11
12
  export function readAbilityDetails(dir) {
12
13
  const manifestPath = join(dir, 'loopengine.ability.json');
@@ -26,6 +27,16 @@ export function readAbilityDetails(dir) {
26
27
  const listed = found.length ? found : [{ name: basename(file).replace(/\.[cm]?[jt]s$/, ''), description: '' }];
27
28
  return listed.map((tool) => (decisions[tool.name] ? { ...tool, decision: decisions[tool.name] } : tool));
28
29
  });
30
+ // An MCP ability's tools are named from its allowlist; descriptions
31
+ // come from the server, so they're unknown until it's connected.
32
+ if (manifest.mcp !== undefined) {
33
+ try {
34
+ for (const tool of resolveMcpManifest(pkg.name || basename(dir), manifest.mcp).tools) {
35
+ tools.push(decisions[tool.name] ? { name: tool.name, description: '', decision: decisions[tool.name] } : { name: tool.name, description: '' });
36
+ }
37
+ }
38
+ catch { }
39
+ }
29
40
  const skills = strings(manifest.skills).map((skillDir) => readSkill(join(dir, skillDir), skillDir));
30
41
  return { name: pkg.name || basename(dir), version: pkg.version || '', description: pkg.description || '', tools, skills, settings: readSettings(manifest) };
31
42
  }
@@ -0,0 +1,91 @@
1
+ import type { AgentEnv, ToolDefinition } from './agent-config.js';
2
+ /** How the adapter authenticates to a remote server. `key` sends an
3
+ * agent setting as a header (`Authorization: Bearer <value>` by
4
+ * default). OAuth isn't supported yet. */
5
+ export type McpAuth = {
6
+ type: 'none';
7
+ } | {
8
+ type: 'key';
9
+ env: string;
10
+ header?: string;
11
+ prefix?: string;
12
+ };
13
+ /** The `mcp` block of loopengine.ability.json. Exactly one of `url`
14
+ * (a remote server) or `command` (a local server, started as a child
15
+ * process) is set. String values in `headers`, `args` and `env` may use
16
+ * `${NAME}` to read an agent setting. */
17
+ export interface McpServerManifest {
18
+ url?: string;
19
+ /** `streamable-http` (the default) or the older `sse` transport. */
20
+ transport?: 'streamable-http' | 'sse';
21
+ headers?: Record<string, string>;
22
+ auth?: McpAuth;
23
+ command?: string;
24
+ args?: string[];
25
+ env?: Record<string, string>;
26
+ /** The server's tool names to expose — nothing else on the server is. */
27
+ tools: string[];
28
+ /** Prefix for the installed tool names (`<prefix>_<tool>`). Defaults to
29
+ * the ability's name without `lp-`. */
30
+ prefix?: string;
31
+ /** Longest tool result passed back to the model, in characters. */
32
+ maxOutputChars?: number;
33
+ /** Tool inputs filled from the agent's settings when a call leaves them
34
+ * out, e.g. a default repository: `{ "repo": "${GITHUB_REPO}" }`, or a
35
+ * part of one setting, `{ "owner": { "env": "GITHUB_DEFAULT_REPO",
36
+ * "split": "/", "part": 0 } }`. An input with a default is no longer
37
+ * required in the schema the model sees, which says what it defaults to. */
38
+ defaults?: Record<string, McpDefault>;
39
+ }
40
+ export type McpDefault = string | {
41
+ env: string;
42
+ split?: string;
43
+ part?: number;
44
+ };
45
+ /** What add-ability records for an MCP ability: the server block, and
46
+ * each installed tool name with the server tool it calls. */
47
+ export interface InstalledMcpRecord {
48
+ server: Omit<McpServerManifest, 'tools' | 'prefix'>;
49
+ tools: Array<{
50
+ name: string;
51
+ remote: string;
52
+ }>;
53
+ }
54
+ export declare class McpManifestError extends Error {
55
+ }
56
+ /** Checks an ability's `mcp` block and works out its installed tool
57
+ * names. Throws McpManifestError on anything unusable, so a bad
58
+ * manifest fails at add-ability, not on the agent's first run. */
59
+ export declare function resolveMcpManifest(abilityName: string, mcp: unknown): InstalledMcpRecord;
60
+ /** An installed tool's name: `<prefix>_<tool>` in snake_case, or just the
61
+ * tool's own name when it already carries the prefix as a word
62
+ * (`web_search_exa` stays as is for prefix `exa`). */
63
+ export declare function mcpToolName(prefix: string, remote: string): string;
64
+ /** Closes every open server connection (and stops local servers). */
65
+ export declare function closeMcpConnections(): Promise<void>;
66
+ /** Whether a connection failed because the server wants (other)
67
+ * credentials: an HTTP 401/403, or the SDK's own UnauthorizedError. */
68
+ export declare function isMcpAuthError(err: unknown): boolean;
69
+ /** A server tool as listed, for choosing tools before an ability exists. */
70
+ export interface McpToolInfo {
71
+ name: string;
72
+ description: string;
73
+ inputSchema: Record<string, unknown>;
74
+ /** The server's own hints (MCP tool annotations), when it gives them. */
75
+ readOnly?: boolean;
76
+ destructive?: boolean;
77
+ }
78
+ /** Every tool a server offers, over a one-off connection (closed after).
79
+ * `server` is an `mcp` block without its allowlist; settings in it are
80
+ * read from `env`. */
81
+ export declare function listMcpServerTools(server: InstalledMcpRecord['server'], env: AgentEnv): Promise<McpToolInfo[]>;
82
+ /** Drops an ability's cached schemas — add-ability and upgrade-ability
83
+ * call this so a new version's tools are fetched fresh. */
84
+ export declare function clearMcpSchemaCache(dir: string, abilityName: string): void;
85
+ export declare function mcpAbilityLoadError(dir: string, abilityName: string): string | undefined;
86
+ /** Every tool of every MCP ability installed in `dir` (an agent's
87
+ * folder). An ability whose schemas aren't cached yet is connected to
88
+ * once to fetch them; if that fails (no key yet, server down) its tools
89
+ * are left out and the reason is kept for mcpAbilityLoadError, rather
90
+ * than failing the whole agent. */
91
+ export declare function loadMcpAbilityTools(dir: string, agentName: string): Promise<ToolDefinition[]>;