ai-runtime-engine 1.2.0 → 2.7.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/CHANGELOG.md +602 -0
- package/dist/agents/admit.d.ts +69 -0
- package/dist/agents/admit.js +129 -0
- package/dist/agents/definition.d.ts +36 -0
- package/dist/agents/definition.js +9 -0
- package/dist/agents/envelope.d.ts +53 -0
- package/dist/agents/envelope.js +68 -0
- package/dist/agents/finding.d.ts +79 -0
- package/dist/agents/finding.js +80 -0
- package/dist/agents/task.d.ts +60 -0
- package/dist/agents/task.js +32 -0
- package/dist/agents/worker.d.ts +68 -0
- package/dist/agents/worker.js +256 -0
- package/dist/capabilities/capability.d.ts +117 -0
- package/dist/capabilities/capability.js +66 -0
- package/dist/capabilities/registry.d.ts +139 -0
- package/dist/capabilities/registry.js +413 -0
- package/dist/capabilities/vocabulary.d.ts +32 -0
- package/dist/capabilities/vocabulary.js +34 -0
- package/dist/cli/cli.js +56 -4
- package/dist/cli/commands/cleanup.js +29 -27
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +38 -8
- package/dist/cli/commands/executions.js +34 -25
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.js +11 -9
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/inspect.d.ts +40 -1
- package/dist/cli/commands/inspect.js +157 -2
- package/dist/cli/commands/mcp.d.ts +45 -0
- package/dist/cli/commands/mcp.js +148 -0
- package/dist/cli/commands/route.js +21 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +36 -4
- package/dist/cli/commands/skills.d.ts +2 -0
- package/dist/cli/commands/skills.js +29 -7
- package/dist/cli/interactive/ansi.d.ts +41 -0
- package/dist/cli/interactive/ansi.js +43 -0
- package/dist/cli/interactive/complete.d.ts +10 -0
- package/dist/cli/interactive/complete.js +19 -0
- package/dist/cli/interactive/repl.d.ts +3 -0
- package/dist/cli/interactive/repl.js +105 -16
- package/dist/cli/interactive/session.d.ts +12 -1
- package/dist/cli/interactive/session.js +83 -5
- package/dist/cli/render.d.ts +13 -0
- package/dist/cli/render.js +18 -0
- package/dist/cli/runtimeSession.d.ts +11 -0
- package/dist/cli/runtimeSession.js +17 -0
- package/dist/config/defaults.d.ts +3 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +2 -2
- package/dist/context/lossVerifier.d.ts +24 -0
- package/dist/context/lossVerifier.js +45 -0
- package/dist/context/summarize.d.ts +19 -0
- package/dist/context/summarize.js +53 -0
- package/dist/core/fallback/fallback.d.ts +8 -0
- package/dist/core/fallback/fallback.js +3 -1
- package/dist/core/router/executor.d.ts +6 -1
- package/dist/core/router/executor.js +9 -2
- package/dist/core/router/normalize.d.ts +2 -0
- package/dist/core/router/request.js +2 -0
- package/dist/core/router/router.d.ts +3 -0
- package/dist/core/router/router.js +7 -0
- package/dist/executions/execution.d.ts +13 -2
- package/dist/generation/generateAdapter.d.ts +14 -0
- package/dist/generation/generateAdapter.js +38 -0
- package/dist/generation/generateSkill.d.ts +26 -0
- package/dist/generation/generateSkill.js +51 -0
- package/dist/index.d.ts +44 -5
- package/dist/index.js +26 -2
- package/dist/mcp/client.d.ts +70 -0
- package/dist/mcp/client.js +221 -0
- package/dist/mcp/manager.d.ts +151 -0
- package/dist/mcp/manager.js +493 -0
- package/dist/mcp/protocol.d.ts +216 -0
- package/dist/mcp/protocol.js +149 -0
- package/dist/mcp/toolAdapter.d.ts +44 -0
- package/dist/mcp/toolAdapter.js +94 -0
- package/dist/mcp/transport.d.ts +109 -0
- package/dist/mcp/transport.js +383 -0
- package/dist/memory/embedders/hash.d.ts +12 -0
- package/dist/memory/embedders/hash.js +31 -0
- package/dist/memory/embedders/http.d.ts +25 -0
- package/dist/memory/embedders/http.js +48 -0
- package/dist/memory/memory.d.ts +19 -2
- package/dist/memory/memory.js +75 -11
- package/dist/memory/semantic.d.ts +17 -0
- package/dist/memory/semantic.js +29 -0
- package/dist/orchestration/budget.d.ts +30 -0
- package/dist/orchestration/budget.js +40 -0
- package/dist/orchestration/executor.d.ts +39 -1
- package/dist/orchestration/executor.js +64 -4
- package/dist/orchestration/orchestrator.d.ts +29 -1
- package/dist/orchestration/orchestrator.js +89 -8
- package/dist/orchestration/plan.d.ts +15 -1
- package/dist/orchestration/plan.js +23 -4
- package/dist/orchestration/planner.d.ts +19 -1
- package/dist/orchestration/planner.js +25 -5
- package/dist/plugin/ai.d.ts +4 -0
- package/dist/plugin/ai.js +9 -0
- package/dist/providers/httpClient.d.ts +25 -1
- package/dist/providers/httpClient.js +93 -0
- package/dist/providers/httpProvider.d.ts +1 -0
- package/dist/providers/httpProvider.js +67 -1
- package/dist/providers/mock/mockProvider.d.ts +3 -0
- package/dist/providers/mock/mockProvider.js +54 -0
- package/dist/providers/mock/scenarios.d.ts +7 -0
- package/dist/providers/provider.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +34 -0
- package/dist/providers/wire/openaiWire.js +30 -0
- package/dist/providers/wire/types.d.ts +16 -0
- package/dist/runtime/config.js +50 -6
- package/dist/runtime/intent/aiClassifier.d.ts +19 -0
- package/dist/runtime/intent/aiClassifier.js +74 -0
- package/dist/runtime/models/modelProfile.d.ts +61 -0
- package/dist/runtime/models/modelProfile.js +139 -0
- package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
- package/dist/runtime/planning/deriveCapabilities.js +146 -0
- package/dist/runtime/policy.d.ts +10 -0
- package/dist/runtime/policy.js +9 -2
- package/dist/runtime/runtime.d.ts +173 -0
- package/dist/runtime/runtime.js +723 -50
- package/dist/runtime/types.d.ts +94 -2
- package/dist/skills/manifest.d.ts +3 -0
- package/dist/skills/manifest.js +24 -0
- package/dist/skills/registry.d.ts +16 -1
- package/dist/skills/registry.js +21 -1
- package/dist/skills/skill.d.ts +6 -1
- package/dist/store/area.d.ts +15 -1
- package/dist/store/area.js +19 -8
- package/dist/store/crypto.d.ts +21 -0
- package/dist/store/crypto.js +49 -0
- package/dist/store/paths.d.ts +5 -1
- package/dist/store/paths.js +6 -0
- package/dist/store/store.d.ts +15 -3
- package/dist/store/store.js +28 -7
- package/dist/telemetry/sinks/otlp.d.ts +31 -0
- package/dist/telemetry/sinks/otlp.js +76 -0
- package/dist/tools/builtins/filesystem.js +1 -0
- package/dist/tools/builtins/git.js +1 -0
- package/dist/tools/builtins/shell.js +1 -0
- package/dist/tools/permissions.d.ts +28 -0
- package/dist/tools/permissions.js +72 -0
- package/dist/tools/registry.d.ts +18 -2
- package/dist/tools/registry.js +22 -2
- package/dist/tools/tool.d.ts +4 -0
- package/dist/types.d.ts +11 -1
- package/dist/util/flatten.d.ts +11 -0
- package/dist/util/flatten.js +18 -0
- package/dist/util/semaphore.d.ts +19 -0
- package/dist/util/semaphore.js +60 -0
- package/package.json +24 -9
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { AIError } from '../core/fallback/errors.js';
|
|
8
8
|
import { analyzeOpenApiFile } from '../discovery/openapi.js';
|
|
9
|
+
import { extractJson } from '../util/extractJson.js';
|
|
10
|
+
import { wrapUntrusted } from '../tools/untrusted.js';
|
|
9
11
|
/** Turn an OpenAPI analysis into a ProviderConfig. Throws if the spec yielded no usable mapping. */
|
|
10
12
|
export function generateProviderConfig(analysis, overrides = {}) {
|
|
11
13
|
const base = analysis.suggestedProviderConfig;
|
|
@@ -28,3 +30,39 @@ export function generateProviderConfig(analysis, overrides = {}) {
|
|
|
28
30
|
export function generateProviderConfigFromFile(specPath, overrides = {}) {
|
|
29
31
|
return generateProviderConfig(analyzeOpenApiFile(specPath), overrides);
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* AI-assisted adapter rung (Phase 21a) — an opt-in gap-filler on top of the DETERMINISTIC
|
|
35
|
+
* `generateProviderConfig`. When the spec doesn't enumerate models, a model proposes a likely model-id
|
|
36
|
+
* list; the OUTPUT is STILL produced by `generateProviderConfig` (a validated `ProviderConfig`, never code),
|
|
37
|
+
* so a bad suggestion can only propose model-id strings the operator then confirms — it can never inject an
|
|
38
|
+
* unvalidated config. Any model/parse failure falls back to the deterministic result (or its error).
|
|
39
|
+
*/
|
|
40
|
+
export async function suggestProviderConfig(analysis, ai, overrides = {}) {
|
|
41
|
+
let modelList = Array.isArray(overrides.models) ? overrides.models : [];
|
|
42
|
+
// Only consult the model when we actually have a gap to fill (no explicit models, none in the spec).
|
|
43
|
+
const specModels = analysis.suggestedProviderConfig?.models;
|
|
44
|
+
const needsModels = modelList.length === 0 && (specModels === undefined || specModels === 'auto');
|
|
45
|
+
if (needsModels) {
|
|
46
|
+
try {
|
|
47
|
+
const prompt = `An OpenAPI spec describes an AI provider. Suggest the model IDs it most likely exposes for chat/completions. ` +
|
|
48
|
+
`Reply ONLY with JSON: {"models": ["<model-id>", ...]}.\n\n` +
|
|
49
|
+
wrapUntrusted('openapi-summary', `title: ${analysis.title ?? '(unknown)'}\nbaseUrl: ${analysis.baseUrl ?? '(unknown)'}\nwireShape: ${analysis.wireShape}\nendpoints: ${analysis.endpoints.map((e) => e.path).slice(0, 20).join(', ')}`);
|
|
50
|
+
const run = await ai.run({ task: 'suggest-provider-config', input: prompt, output: { format: 'json' }, requirements: { intelligence: ['reasoning'], output: ['json'] } });
|
|
51
|
+
if (run.ok && run.response) {
|
|
52
|
+
const parsed = (run.response.json ?? extractJson(run.response.text ?? ''));
|
|
53
|
+
if (Array.isArray(parsed?.models))
|
|
54
|
+
modelList = parsed.models.filter((m) => typeof m === 'string' && m.trim().length > 0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* a suggestion failure just falls through to the deterministic path */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const config = generateProviderConfig(analysis, { ...overrides, ...(modelList.length && !overrides.models ? { models: modelList } : {}) });
|
|
63
|
+
return { ok: true, config };
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill scaffolding (Phase 21a) — the generative seam for skills, mirroring `generateAdapter`'s
|
|
3
|
+
* "emit validated CONFIG/manifest, never code" rule. A model drafts a `*.skill.yaml` MANIFEST (pure data,
|
|
4
|
+
* parsed by the existing `parseManifest`); we then validate it three ways before offering it:
|
|
5
|
+
* 1. `parseManifest` (shape),
|
|
6
|
+
* 2. every step names an AVAILABLE tool (a `validatePlan`-style membership check — no invented tools),
|
|
7
|
+
* 3. a dry `manifestToSkill` compile (it must not throw).
|
|
8
|
+
* Nothing is written here — the caller writes it only on explicit confirm into the consented
|
|
9
|
+
* `.ai-runtime/skills/` directory. A drafted manifest that fails any check is refused with a reason.
|
|
10
|
+
*/
|
|
11
|
+
import type { AI } from '../plugin/ai.js';
|
|
12
|
+
import { type SkillManifest } from '../skills/manifest.js';
|
|
13
|
+
export interface GenerateSkillInput {
|
|
14
|
+
goal: string;
|
|
15
|
+
/** Tool ids the drafted manifest may use (the host's registered tools). */
|
|
16
|
+
tools: string[];
|
|
17
|
+
ai: AI;
|
|
18
|
+
}
|
|
19
|
+
export interface GenerateSkillResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
manifest?: SkillManifest;
|
|
22
|
+
/** The manifest serialized as `*.skill.yaml`, ready to write on confirm. */
|
|
23
|
+
yaml?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function generateSkillManifest(input: GenerateSkillInput): Promise<GenerateSkillResult>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill scaffolding (Phase 21a) — the generative seam for skills, mirroring `generateAdapter`'s
|
|
3
|
+
* "emit validated CONFIG/manifest, never code" rule. A model drafts a `*.skill.yaml` MANIFEST (pure data,
|
|
4
|
+
* parsed by the existing `parseManifest`); we then validate it three ways before offering it:
|
|
5
|
+
* 1. `parseManifest` (shape),
|
|
6
|
+
* 2. every step names an AVAILABLE tool (a `validatePlan`-style membership check — no invented tools),
|
|
7
|
+
* 3. a dry `manifestToSkill` compile (it must not throw).
|
|
8
|
+
* Nothing is written here — the caller writes it only on explicit confirm into the consented
|
|
9
|
+
* `.ai-runtime/skills/` directory. A drafted manifest that fails any check is refused with a reason.
|
|
10
|
+
*/
|
|
11
|
+
import { stringify as toYaml } from 'yaml';
|
|
12
|
+
import { parseManifest, manifestToSkill } from '../skills/manifest.js';
|
|
13
|
+
import { wrapUntrusted } from '../tools/untrusted.js';
|
|
14
|
+
import { extractJson } from '../util/extractJson.js';
|
|
15
|
+
export async function generateSkillManifest(input) {
|
|
16
|
+
const goal = String(input.goal ?? '').trim();
|
|
17
|
+
if (!goal)
|
|
18
|
+
return { ok: false, error: 'a goal is required' };
|
|
19
|
+
const tools = input.tools ?? [];
|
|
20
|
+
if (tools.length === 0)
|
|
21
|
+
return { ok: false, error: 'no tools are available to compose a skill from' };
|
|
22
|
+
const prompt = `Draft an ai-runtime skill MANIFEST (pure data — NOT code) that achieves the goal below. ` +
|
|
23
|
+
`The manifest runs a sequence of tool steps. Use ONLY these registered tools: ${tools.join(', ')}.\n\n` +
|
|
24
|
+
`Goal:\n${wrapUntrusted('scaffold-goal', goal)}\n\n` +
|
|
25
|
+
`Reply ONLY with JSON of this exact shape: {"id":"<kebab-case-id>","version":"1.0.0","name":"<short name>",` +
|
|
26
|
+
`"description":"<one line>","runtimeCompatibility":">=1.0.0","steps":[{"tool":"<one of the tools>","description":"<what this step does>"}]}.`;
|
|
27
|
+
// The whole model round-trip is wrapped: a bad model reply (e.g. non-JSON, which makes `extractJson`
|
|
28
|
+
// throw) must surface as { ok:false }, never as a thrown error — scaffolding is an assist, not a hard op.
|
|
29
|
+
try {
|
|
30
|
+
const run = await input.ai.run({ task: 'generate-skill', input: prompt, output: { format: 'json' }, requirements: { intelligence: ['reasoning'], output: ['json'] } });
|
|
31
|
+
if (!run.ok || !run.response)
|
|
32
|
+
return { ok: false, error: `generation failed (${run.error?.category ?? 'no response'})` };
|
|
33
|
+
const raw = run.response.json ?? extractJson(run.response.text ?? '');
|
|
34
|
+
const { manifest, error } = parseManifest(raw);
|
|
35
|
+
if (!manifest)
|
|
36
|
+
return { ok: false, error: `drafted manifest is invalid: ${error ?? 'unparseable'}` };
|
|
37
|
+
// Membership: a drafted step may only name a tool the host actually has (no invented capabilities).
|
|
38
|
+
const unknown = [...new Set(manifest.steps.map((s) => s.tool).filter((t) => !tools.includes(t)))];
|
|
39
|
+
if (unknown.length)
|
|
40
|
+
return { ok: false, error: `manifest names unregistered tool(s): ${unknown.join(', ')}` };
|
|
41
|
+
// A safe, filesystem-friendly id (also the filename); reject anything that could escape the skills dir.
|
|
42
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(manifest.id))
|
|
43
|
+
return { ok: false, error: `manifest id must be kebab-case ([a-z0-9-]): got ${JSON.stringify(manifest.id)}` };
|
|
44
|
+
// Dry compile — the manifest must produce a runnable Skill without throwing.
|
|
45
|
+
manifestToSkill(manifest, '(scaffold)');
|
|
46
|
+
return { ok: true, manifest, yaml: toYaml(manifest) };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { ok: false, error: `could not scaffold a skill: ${e instanceof Error ? e.message : String(e)}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { FetchLike } from './providers/httpClient.js';
|
|
|
20
20
|
export { PROVIDER_DEFAULTS } from './config/providerDefaults.js';
|
|
21
21
|
export { resolveModelMetadata } from './discovery/modelCatalog.js';
|
|
22
22
|
export { registerWire } from './providers/wire/registry.js';
|
|
23
|
-
export type { WireModule, WireShape } from './providers/wire/types.js';
|
|
23
|
+
export type { WireDelta, WireModule, WireShape } from './providers/wire/types.js';
|
|
24
24
|
export { AIError, isRetryable, statusToCategory, toAIError } from './core/fallback/errors.js';
|
|
25
25
|
export { capabilitySatisfies, emptyProfile, getCapability, mergeProfiles, profileFromDeclared, rankOf, } from './core/capabilities/evidence.js';
|
|
26
26
|
export { AGENT_CAPABILITIES, INPUT_MODALITIES, INTELLIGENCE_SKILLS, OUTPUT_MODALITIES, } from './core/capabilities/taxonomy.js';
|
|
@@ -44,14 +44,20 @@ export { analyzeOpenApi, analyzeOpenApiFile } from './discovery/openapi.js';
|
|
|
44
44
|
export type { OpenApiAnalysis } from './discovery/openapi.js';
|
|
45
45
|
export { benchmark } from './benchmark/benchmark.js';
|
|
46
46
|
export type { BenchResult, BenchTaskSpec } from './benchmark/benchmark.js';
|
|
47
|
-
export { generateProviderConfig, generateProviderConfigFromFile } from './generation/generateAdapter.js';
|
|
48
|
-
export type { GenerateOptions } from './generation/generateAdapter.js';
|
|
47
|
+
export { generateProviderConfig, generateProviderConfigFromFile, suggestProviderConfig } from './generation/generateAdapter.js';
|
|
48
|
+
export type { GenerateOptions, SuggestResult } from './generation/generateAdapter.js';
|
|
49
|
+
export { generateSkillManifest } from './generation/generateSkill.js';
|
|
50
|
+
export type { GenerateSkillInput, GenerateSkillResult } from './generation/generateSkill.js';
|
|
49
51
|
export { McpRegistry, StaticMcpSource } from './mcp/mcp.js';
|
|
50
52
|
export type { McpToolSource } from './mcp/mcp.js';
|
|
51
53
|
export { setCredentialResolver, Credential } from './security/credentials.js';
|
|
52
54
|
export type { CredentialResolver } from './security/credentials.js';
|
|
53
55
|
export { CallbackSink, MemorySink, MultiSink, NullSink } from './telemetry/telemetry.js';
|
|
54
56
|
export type { TelemetrySink } from './telemetry/telemetry.js';
|
|
57
|
+
export { FileSink } from './telemetry/sinks/file.js';
|
|
58
|
+
export { OtlpSink } from './telemetry/sinks/otlp.js';
|
|
59
|
+
export type { OtlpSinkOptions } from './telemetry/sinks/otlp.js';
|
|
60
|
+
export { KeyedSemaphore } from './util/semaphore.js';
|
|
55
61
|
export { MARKETPLACE, listPresets, findPreset, presetToConfig } from './marketplace/presets.js';
|
|
56
62
|
export type { ProviderPreset } from './marketplace/presets.js';
|
|
57
63
|
export { loadRemoteConfig, loadConfigAsync } from './config/load.js';
|
|
@@ -64,6 +70,7 @@ export { resolvePolicy, defaultAutonomyForMode } from './runtime/policy.js';
|
|
|
64
70
|
export type { ExecutionPolicy, PermissionPolicy, Autonomy, ApprovalLevel, PolicyOverrides } from './runtime/policy.js';
|
|
65
71
|
export { parseRuntimeConfig, loadRuntimeConfig, findRuntimeConfigFile } from './runtime/config.js';
|
|
66
72
|
export { HeuristicIntentClassifier } from './runtime/intent/classifier.js';
|
|
73
|
+
export { AIIntentClassifier } from './runtime/intent/aiClassifier.js';
|
|
67
74
|
export type { IntentClassifier, IntentClassification, IntentInput } from './runtime/intent/classifier.js';
|
|
68
75
|
export { detectWorkspace, summarizeWorkspace } from './runtime/workspace/workspace.js';
|
|
69
76
|
export type { WorkspaceInfo } from './runtime/workspace/workspace.js';
|
|
@@ -77,8 +84,29 @@ export type { AccessState, ProviderView, ModelView, ProviderViewSource } from '.
|
|
|
77
84
|
export { RuntimeStore, STORE_VERSION } from './store/store.js';
|
|
78
85
|
export type { RuntimeStoreOptions, StorePaths } from './store/store.js';
|
|
79
86
|
export { FileArea, NullArea } from './store/area.js';
|
|
80
|
-
export type { Area, IntegrityIssue } from './store/area.js';
|
|
81
|
-
export {
|
|
87
|
+
export type { Area, IntegrityIssue, ContentCodec } from './store/area.js';
|
|
88
|
+
export { makeCodec, deriveKey, encryptString, decryptString, ENVELOPE_PREFIX } from './store/crypto.js';
|
|
89
|
+
export { resolveHome, projectId, repositoryId, organizationId, findRepoRoot } from './store/paths.js';
|
|
90
|
+
export { deriveCapabilities, deriveCapabilitiesOffline, candidatesFrom, candidateSlate, DERIVE_MAX_CANDIDATES, DERIVE_MAX_IDS } from './runtime/planning/deriveCapabilities.js';
|
|
91
|
+
export type { CapabilityCandidate, DeriveCapabilitiesInput, DeriveCapabilitiesResult } from './runtime/planning/deriveCapabilities.js';
|
|
92
|
+
export { ActionCapabilityRegistry, capabilityReportFrom } from './capabilities/registry.js';
|
|
93
|
+
export type { ActionCapabilityQuery, CapabilityConfig, RegistryAccessors } from './capabilities/registry.js';
|
|
94
|
+
export { CURATED_VOCABULARY, curated, isCurated } from './capabilities/vocabulary.js';
|
|
95
|
+
export type { CuratedCapability, PermissionHint } from './capabilities/vocabulary.js';
|
|
96
|
+
export type { ActionCapability, ActionCapabilityDecl, ActionCapabilityEffect, ActionCapabilityProvider, ActionCapabilitySource, CapabilityGap, CapabilityGapReason, CapabilityResolution, GapSuggestion, MissingCapabilityReport, ProviderAvailability, } from './capabilities/capability.js';
|
|
97
|
+
export type { AgentDefinition } from './agents/definition.js';
|
|
98
|
+
export type { AgentEnvelope } from './agents/envelope.js';
|
|
99
|
+
export type { AgentTaskRecord, AgentTaskState } from './agents/task.js';
|
|
100
|
+
export type { Finding, FindingEvidence, FindingEvidenceKind, FindingStatus } from './agents/finding.js';
|
|
101
|
+
export type { AdmissionRejection, AdmissionResult } from './agents/admit.js';
|
|
102
|
+
export type { StepObservationCode } from './orchestration/executor.js';
|
|
103
|
+
export { PLAN_STEP_STATUSES } from './orchestration/plan.js';
|
|
104
|
+
export { mcpToolId } from './mcp/toolAdapter.js';
|
|
105
|
+
export { MCP_PROTOCOL_VERSION } from './mcp/protocol.js';
|
|
106
|
+
export type { McpServerConfig, McpServerState, McpServerStatus, McpTransportKind } from './mcp/manager.js';
|
|
107
|
+
export type { McpServerSettings } from './runtime/types.js';
|
|
108
|
+
export type { McpServerGrant } from './tools/permissions.js';
|
|
109
|
+
export { clampMcpPermissions, clampPermissions } from './tools/permissions.js';
|
|
82
110
|
export type { StoreScope } from './store/paths.js';
|
|
83
111
|
export { ConversationStore } from './conversations/conversations.js';
|
|
84
112
|
export type { ConversationTurn, ConversationMeta, TurnRole } from './conversations/conversations.js';
|
|
@@ -87,9 +115,18 @@ export type { MemoryRecord, MemoryScope, MemorySource, RememberInput, SearchOpti
|
|
|
87
115
|
export { classifyMemory } from './memory/classifier.js';
|
|
88
116
|
export type { MemoryCandidate } from './memory/classifier.js';
|
|
89
117
|
export { bm25Search, tokenize } from './memory/bm25.js';
|
|
118
|
+
export { cosineSimilarity, semanticRank } from './memory/semantic.js';
|
|
119
|
+
export type { VecDoc, RankedHit } from './memory/semantic.js';
|
|
120
|
+
export { HashEmbedder } from './memory/embedders/hash.js';
|
|
121
|
+
export { HttpEmbedder } from './memory/embedders/http.js';
|
|
122
|
+
export type { HttpEmbedderConfig } from './memory/embedders/http.js';
|
|
90
123
|
export type { MemoryTrace } from './runtime/types.js';
|
|
91
124
|
export { compileContext } from './context/compiler.js';
|
|
92
125
|
export type { ContextBlock, CompiledBlock, CompiledContext, CompileMetrics, CompileOptions, ContextValidation, RetentionLevel } from './context/compiler.js';
|
|
126
|
+
export { verifyContextLoss } from './context/lossVerifier.js';
|
|
127
|
+
export type { VerifyLossInput, LossCheck } from './context/lossVerifier.js';
|
|
128
|
+
export { summarizeOverBudget } from './context/summarize.js';
|
|
129
|
+
export type { SummarizeInput } from './context/summarize.js';
|
|
93
130
|
export { TokenEstimator, estimateTokens } from './context/tokens.js';
|
|
94
131
|
export { resolveContextBudget, DEFAULT_CONTEXT_TOKENS } from './context/budget.js';
|
|
95
132
|
export type { ContextBudgetInputs } from './context/budget.js';
|
|
@@ -141,6 +178,8 @@ export type { LearningOutcome, LearnedStat, SkillRef } from './learning/learning
|
|
|
141
178
|
export { classifyFeedback } from './learning/feedback.js';
|
|
142
179
|
export type { FeedbackSignal } from './learning/feedback.js';
|
|
143
180
|
export { resolveRoutingPrefs, withPreferredProviders } from './runtime/routing.js';
|
|
181
|
+
export { parseModelProfile, parseDirective, resolveModelDirective, directiveToOverrides, loadModelProfile } from './runtime/models/modelProfile.js';
|
|
182
|
+
export type { ModelProfile, ModelDirective } from './runtime/models/modelProfile.js';
|
|
144
183
|
export type { RoutingSources } from './runtime/routing.js';
|
|
145
184
|
export { isExcluded, isPreferred } from './core/router/routingPrefs.js';
|
|
146
185
|
export type { RoutingPreferences } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -40,10 +40,14 @@ export { runVerification } from './verification/verify.js';
|
|
|
40
40
|
export { analyzeOpenApi, analyzeOpenApiFile } from './discovery/openapi.js';
|
|
41
41
|
export { benchmark } from './benchmark/benchmark.js';
|
|
42
42
|
// Enterprise & extensibility — dynamic adapters, MCP, enterprise credentials, observability, policy, marketplace, remote
|
|
43
|
-
export { generateProviderConfig, generateProviderConfigFromFile } from './generation/generateAdapter.js';
|
|
43
|
+
export { generateProviderConfig, generateProviderConfigFromFile, suggestProviderConfig } from './generation/generateAdapter.js';
|
|
44
|
+
export { generateSkillManifest } from './generation/generateSkill.js';
|
|
44
45
|
export { McpRegistry, StaticMcpSource } from './mcp/mcp.js';
|
|
45
46
|
export { setCredentialResolver, Credential } from './security/credentials.js';
|
|
46
47
|
export { CallbackSink, MemorySink, MultiSink, NullSink } from './telemetry/telemetry.js';
|
|
48
|
+
export { FileSink } from './telemetry/sinks/file.js';
|
|
49
|
+
export { OtlpSink } from './telemetry/sinks/otlp.js';
|
|
50
|
+
export { KeyedSemaphore } from './util/semaphore.js';
|
|
47
51
|
export { MARKETPLACE, listPresets, findPreset, presetToConfig } from './marketplace/presets.js';
|
|
48
52
|
export { loadRemoteConfig, loadConfigAsync } from './config/load.js';
|
|
49
53
|
// ── Runtime (Phase 1) — high-level facade over the router: modes, workspace, policy ──
|
|
@@ -52,6 +56,7 @@ export { RUNTIME_MODES } from './runtime/types.js';
|
|
|
52
56
|
export { resolvePolicy, defaultAutonomyForMode } from './runtime/policy.js';
|
|
53
57
|
export { parseRuntimeConfig, loadRuntimeConfig, findRuntimeConfigFile } from './runtime/config.js';
|
|
54
58
|
export { HeuristicIntentClassifier } from './runtime/intent/classifier.js';
|
|
59
|
+
export { AIIntentClassifier } from './runtime/intent/aiClassifier.js';
|
|
55
60
|
export { detectWorkspace, summarizeWorkspace } from './runtime/workspace/workspace.js';
|
|
56
61
|
export { MODE_AVAILABILITY, IMPLEMENTED_MODES } from './runtime/modes/availability.js';
|
|
57
62
|
export { CHAT_TASK } from './runtime/modes/chat.js';
|
|
@@ -61,13 +66,31 @@ export { deriveAccessState, buildProviderViews, ProviderViewCache } from './runt
|
|
|
61
66
|
// ── Runtime (Phase 3) — local store, conversations, memory ──
|
|
62
67
|
export { RuntimeStore, STORE_VERSION } from './store/store.js';
|
|
63
68
|
export { FileArea, NullArea } from './store/area.js';
|
|
64
|
-
export {
|
|
69
|
+
export { makeCodec, deriveKey, encryptString, decryptString, ENVELOPE_PREFIX } from './store/crypto.js';
|
|
70
|
+
export { resolveHome, projectId, repositoryId, organizationId, findRepoRoot } from './store/paths.js';
|
|
71
|
+
// ── Capability-first planning (Phase 3.3) — offline BM25 derivation, one model rung under it ──
|
|
72
|
+
export { deriveCapabilities, deriveCapabilitiesOffline, candidatesFrom, candidateSlate, DERIVE_MAX_CANDIDATES, DERIVE_MAX_IDS } from './runtime/planning/deriveCapabilities.js';
|
|
73
|
+
// ── Action capabilities (Phase 3.1) — what the runtime can DO, and who provides it ──
|
|
74
|
+
export { ActionCapabilityRegistry, capabilityReportFrom } from './capabilities/registry.js';
|
|
75
|
+
export { CURATED_VOCABULARY, curated, isCurated } from './capabilities/vocabulary.js';
|
|
76
|
+
export { PLAN_STEP_STATUSES } from './orchestration/plan.js';
|
|
77
|
+
// ── MCP connectivity (Phase 3.2) — external servers as ordinary Runtime tools ──
|
|
78
|
+
// The CONFIG + STATUS surface is public; the client, transports, and manager internals are not, so the
|
|
79
|
+
// wire implementation stays free to change without a breaking release.
|
|
80
|
+
export { mcpToolId } from './mcp/toolAdapter.js';
|
|
81
|
+
export { MCP_PROTOCOL_VERSION } from './mcp/protocol.js';
|
|
82
|
+
export { clampMcpPermissions, clampPermissions } from './tools/permissions.js';
|
|
65
83
|
export { ConversationStore } from './conversations/conversations.js';
|
|
66
84
|
export { MemoryStore } from './memory/memory.js';
|
|
67
85
|
export { classifyMemory } from './memory/classifier.js';
|
|
68
86
|
export { bm25Search, tokenize } from './memory/bm25.js';
|
|
87
|
+
export { cosineSimilarity, semanticRank } from './memory/semantic.js';
|
|
88
|
+
export { HashEmbedder } from './memory/embedders/hash.js';
|
|
89
|
+
export { HttpEmbedder } from './memory/embedders/http.js';
|
|
69
90
|
// ── Runtime (Phase 4) — context compiler + token budgets ──
|
|
70
91
|
export { compileContext } from './context/compiler.js';
|
|
92
|
+
export { verifyContextLoss } from './context/lossVerifier.js';
|
|
93
|
+
export { summarizeOverBudget } from './context/summarize.js';
|
|
71
94
|
export { TokenEstimator, estimateTokens } from './context/tokens.js';
|
|
72
95
|
export { resolveContextBudget, DEFAULT_CONTEXT_TOKENS } from './context/budget.js';
|
|
73
96
|
// ── Runtime (Phase 5) — tools, permissions, skills, untrusted-data boundary ──
|
|
@@ -104,4 +127,5 @@ export { renderComparison } from './comparison/render.js';
|
|
|
104
127
|
export { LearningStore } from './learning/learningStore.js';
|
|
105
128
|
export { classifyFeedback } from './learning/feedback.js';
|
|
106
129
|
export { resolveRoutingPrefs, withPreferredProviders } from './runtime/routing.js';
|
|
130
|
+
export { parseModelProfile, parseDirective, resolveModelDirective, directiveToOverrides, loadModelProfile } from './runtime/models/modelProfile.js';
|
|
107
131
|
export { isExcluded, isPreferred } from './core/router/routingPrefs.js';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client (Phase 3.2) — the JSON-RPC conversation over any `McpTransport`: `initialize` (with
|
|
3
|
+
* version negotiation), `tools/list` (bounded pagination), `tools/call` (per-call timeout + abort), and
|
|
4
|
+
* `ping`. Requests are correlated by a monotonic id through a pending map, so concurrent calls to one
|
|
5
|
+
* server multiplex correctly.
|
|
6
|
+
*
|
|
7
|
+
* Every thrown error is an `AIError` with a redacted, URL-free message and a mapped category, so a
|
|
8
|
+
* caller (the manager → the Tool adapter) can classify auth vs timeout vs transport without ever seeing
|
|
9
|
+
* a credential or an endpoint.
|
|
10
|
+
*/
|
|
11
|
+
import type { Clock } from '../util/clock.js';
|
|
12
|
+
import type { McpTransport } from './transport.js';
|
|
13
|
+
import type { McpCallResult, NormalizedTool } from './protocol.js';
|
|
14
|
+
export interface McpClientOptions {
|
|
15
|
+
serverId: string;
|
|
16
|
+
clock?: Clock;
|
|
17
|
+
/** Default per-request timeout (ms). */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Max `tools/list` pages to follow — a malicious server must not loop us forever. */
|
|
20
|
+
maxPages?: number;
|
|
21
|
+
/** Max tools accepted from one server (registry-flood bound). */
|
|
22
|
+
maxTools?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface McpHandshake {
|
|
25
|
+
protocolVersion: string;
|
|
26
|
+
serverName?: string;
|
|
27
|
+
serverVersion?: string;
|
|
28
|
+
/** True when the server answered with a version we did not ask for (we still proceed, marked degraded). */
|
|
29
|
+
versionMismatch: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface McpToolList {
|
|
32
|
+
tools: NormalizedTool[];
|
|
33
|
+
/** Declarations that failed validation/normalization and were NOT registered. */
|
|
34
|
+
rejected: number;
|
|
35
|
+
/** True when pagination hit `maxPages`/`maxTools` and the list is therefore partial. */
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare class McpClient {
|
|
39
|
+
private readonly transport;
|
|
40
|
+
private readonly opts;
|
|
41
|
+
private nextId;
|
|
42
|
+
private readonly pending;
|
|
43
|
+
private readonly clock;
|
|
44
|
+
private started;
|
|
45
|
+
/** Errors the server sent with no correlatable id, plus discovery anomalies — surfaced as diagnostics. */
|
|
46
|
+
private readonly protocolErrors;
|
|
47
|
+
constructor(transport: McpTransport, opts: McpClientOptions);
|
|
48
|
+
private onMessage;
|
|
49
|
+
/** Drop a pending entry and release everything attached to it (timer + abort listener). */
|
|
50
|
+
private settle;
|
|
51
|
+
private request;
|
|
52
|
+
/** Connect + negotiate. A counter-proposed version is accepted but reported as a mismatch. */
|
|
53
|
+
initialize(): Promise<McpHandshake>;
|
|
54
|
+
/** List tools, following `nextCursor` up to `maxPages`. Every declaration is normalized (or rejected). */
|
|
55
|
+
listTools(opts?: {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}): Promise<McpToolList>;
|
|
58
|
+
callTool(name: string, args: unknown, opts?: {
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}): Promise<McpCallResult>;
|
|
62
|
+
ping(): Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
latencyMs: number;
|
|
65
|
+
}>;
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
diagnostics(): string[];
|
|
68
|
+
/** Whether the transport says our session is gone and only a fresh handshake can recover. */
|
|
69
|
+
sessionExpired(): boolean;
|
|
70
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client (Phase 3.2) — the JSON-RPC conversation over any `McpTransport`: `initialize` (with
|
|
3
|
+
* version negotiation), `tools/list` (bounded pagination), `tools/call` (per-call timeout + abort), and
|
|
4
|
+
* `ping`. Requests are correlated by a monotonic id through a pending map, so concurrent calls to one
|
|
5
|
+
* server multiplex correctly.
|
|
6
|
+
*
|
|
7
|
+
* Every thrown error is an `AIError` with a redacted, URL-free message and a mapped category, so a
|
|
8
|
+
* caller (the manager → the Tool adapter) can classify auth vs timeout vs transport without ever seeing
|
|
9
|
+
* a credential or an endpoint.
|
|
10
|
+
*/
|
|
11
|
+
import { AIError } from '../core/fallback/errors.js';
|
|
12
|
+
import { redactString } from '../security/redact.js';
|
|
13
|
+
import { systemClock } from '../util/clock.js';
|
|
14
|
+
import { clampText, MCP_CLIENT_INFO, MCP_PROTOCOL_VERSION, JSONRPC_AUTH_CODES, initializeResult, listToolsResult, normalizeToolDecl, flattenCallResult, } from './protocol.js';
|
|
15
|
+
export class McpClient {
|
|
16
|
+
transport;
|
|
17
|
+
opts;
|
|
18
|
+
nextId = 1;
|
|
19
|
+
pending = new Map();
|
|
20
|
+
clock;
|
|
21
|
+
started = false;
|
|
22
|
+
/** Errors the server sent with no correlatable id, plus discovery anomalies — surfaced as diagnostics. */
|
|
23
|
+
protocolErrors = [];
|
|
24
|
+
constructor(transport, opts) {
|
|
25
|
+
this.transport = transport;
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
this.clock = opts.clock ?? systemClock;
|
|
28
|
+
this.transport.onMessage((msg) => this.onMessage(msg));
|
|
29
|
+
}
|
|
30
|
+
onMessage(msg) {
|
|
31
|
+
// A RESPONSE carries an id and exactly one of result/error, and never a method. A server-initiated
|
|
32
|
+
// REQUEST also carries an id — correlating on the id alone would resolve our pending call with
|
|
33
|
+
// `undefined` the moment a server sends one (sampling, roots, elicitation all do).
|
|
34
|
+
const m = msg;
|
|
35
|
+
if (typeof m.method === 'string')
|
|
36
|
+
return; // a request or notification from the server — not ours
|
|
37
|
+
if (m.id === undefined || m.id === null) {
|
|
38
|
+
// A parse/invalid-request error has no id, so it cannot be correlated. Recording it is the honest
|
|
39
|
+
// outcome: the affected request still times out, but the reason is visible in diagnostics.
|
|
40
|
+
if (m.error)
|
|
41
|
+
this.protocolErrors.push(clampText(`unattributable error ${m.error.code}: ${redactString(String(m.error.message ?? ''))}`, 200));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// We only ever send numeric ids; a server that echoes ours as a string is still talking to us.
|
|
45
|
+
const key = typeof m.id === 'number' ? m.id : Number(m.id);
|
|
46
|
+
if (!Number.isFinite(key))
|
|
47
|
+
return;
|
|
48
|
+
const p = this.pending.get(key);
|
|
49
|
+
if (!p)
|
|
50
|
+
return;
|
|
51
|
+
this.settle(key, p);
|
|
52
|
+
if (m.error) {
|
|
53
|
+
const category = JSONRPC_AUTH_CODES.has(m.error.code) ? 'AUTHENTICATION' : 'PROVIDER';
|
|
54
|
+
// The message is server-controlled: redacted, flattened, and clamped before it can reach a status
|
|
55
|
+
// line, a diagnostic, or a replan prompt.
|
|
56
|
+
p.reject(new AIError(`MCP '${this.opts.serverId}' error ${m.error.code}: ${clampText(redactString(String(m.error.message ?? '')), 200)}`, { category, retryable: category !== 'AUTHENTICATION' }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
p.resolve(m.result);
|
|
60
|
+
}
|
|
61
|
+
/** Drop a pending entry and release everything attached to it (timer + abort listener). */
|
|
62
|
+
settle(id, p) {
|
|
63
|
+
this.pending.delete(id);
|
|
64
|
+
clearTimeout(p.timer);
|
|
65
|
+
p.detach?.(); // an abort listener outlives `{ once: true }` when the signal never fires
|
|
66
|
+
}
|
|
67
|
+
async request(method, params, opts = {}) {
|
|
68
|
+
const id = this.nextId++;
|
|
69
|
+
const req = { jsonrpc: '2.0', id, method, ...(params !== undefined ? { params } : {}) };
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? 30_000;
|
|
71
|
+
// NOTE: `result` is not awaited until `send()` returns, so a timeout that fires DURING send would
|
|
72
|
+
// otherwise be an unhandled rejection (fatal by default). A no-op catch marks it handled; the
|
|
73
|
+
// caller still observes the rejection through the returned promise.
|
|
74
|
+
// An already-aborted caller must not reach the wire at all: sending the request and THEN cancelling
|
|
75
|
+
// it would run the remote tool, and `notifications/cancelled` for an id we never issued is noise.
|
|
76
|
+
if (opts.signal?.aborted)
|
|
77
|
+
throw new AIError(`MCP '${this.opts.serverId}' request cancelled`, { category: 'TIMEOUT', retryable: false });
|
|
78
|
+
let sent = false;
|
|
79
|
+
const result = new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.pending.delete(id);
|
|
82
|
+
reject(new AIError(`MCP '${this.opts.serverId}' request timed out after ${timeoutMs}ms (${method})`, { category: 'TIMEOUT', retryable: true }));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
// NOT unref'd on purpose: the timer is how a stalled server becomes a TIMEOUT error. If it were
|
|
85
|
+
// unref'd and the loop drained (stdio child gone), the caller's await would never settle at all.
|
|
86
|
+
const entry = { resolve, reject, timer };
|
|
87
|
+
this.pending.set(id, entry);
|
|
88
|
+
if (opts.signal) {
|
|
89
|
+
const signal = opts.signal;
|
|
90
|
+
const onAbort = () => {
|
|
91
|
+
const p = this.pending.get(id);
|
|
92
|
+
if (!p)
|
|
93
|
+
return;
|
|
94
|
+
this.settle(id, p);
|
|
95
|
+
// Cooperative cancel, but only for a request that actually reached the wire.
|
|
96
|
+
if (sent)
|
|
97
|
+
void this.transport.send({ jsonrpc: '2.0', method: 'notifications/cancelled', params: { requestId: id } }).catch(() => { });
|
|
98
|
+
p.reject(new AIError(`MCP '${this.opts.serverId}' request cancelled`, { category: 'TIMEOUT', retryable: false }));
|
|
99
|
+
};
|
|
100
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
101
|
+
entry.detach = () => signal.removeEventListener('abort', onAbort);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
result.catch(() => { });
|
|
105
|
+
try {
|
|
106
|
+
await this.transport.send(req);
|
|
107
|
+
sent = true;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
// The request never reached the wire: settle and drop the pending entry NOW. Leaving it behind
|
|
111
|
+
// would strand its timer (holding the process open) and fire an unhandled rejection later.
|
|
112
|
+
const p = this.pending.get(id);
|
|
113
|
+
if (p) {
|
|
114
|
+
this.settle(id, p);
|
|
115
|
+
p.reject(e);
|
|
116
|
+
}
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/** Connect + negotiate. A counter-proposed version is accepted but reported as a mismatch. */
|
|
122
|
+
async initialize() {
|
|
123
|
+
if (!this.started) {
|
|
124
|
+
await this.transport.start();
|
|
125
|
+
this.started = true;
|
|
126
|
+
}
|
|
127
|
+
const raw = await this.request('initialize', {
|
|
128
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
129
|
+
capabilities: {},
|
|
130
|
+
clientInfo: MCP_CLIENT_INFO,
|
|
131
|
+
});
|
|
132
|
+
const parsed = initializeResult.safeParse(raw ?? {});
|
|
133
|
+
const data = parsed.success ? parsed.data : {};
|
|
134
|
+
// The negotiated version is echoed into status lines and error text, so it is clamped like any
|
|
135
|
+
// other server string (a "version" of 4 KB of prose is otherwise a free prompt-injection channel).
|
|
136
|
+
const version = typeof data.protocolVersion === 'string' && data.protocolVersion ? clampText(data.protocolVersion, 40) : MCP_PROTOCOL_VERSION;
|
|
137
|
+
// The spec requires MCP-Protocol-Version on every request AFTER the handshake; the transport owns
|
|
138
|
+
// the header, so tell it what we settled on before the first post-handshake call.
|
|
139
|
+
this.transport.setProtocolVersion?.(version);
|
|
140
|
+
// Per spec the client confirms readiness with a notification (servers may ignore it).
|
|
141
|
+
await this.transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' }).catch(() => { });
|
|
142
|
+
return {
|
|
143
|
+
protocolVersion: version,
|
|
144
|
+
...(data.serverInfo?.name ? { serverName: clampText(data.serverInfo.name, 80) } : {}),
|
|
145
|
+
...(data.serverInfo?.version ? { serverVersion: clampText(data.serverInfo.version, 40) } : {}),
|
|
146
|
+
versionMismatch: version !== MCP_PROTOCOL_VERSION,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** List tools, following `nextCursor` up to `maxPages`. Every declaration is normalized (or rejected). */
|
|
150
|
+
async listTools(opts = {}) {
|
|
151
|
+
const maxPages = this.opts.maxPages ?? 20;
|
|
152
|
+
const maxTools = this.opts.maxTools ?? 500;
|
|
153
|
+
const tools = [];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
let rejected = 0;
|
|
156
|
+
let truncated = false;
|
|
157
|
+
let cursor;
|
|
158
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
159
|
+
const raw = await this.request('tools/list', cursor ? { cursor } : {}, opts);
|
|
160
|
+
const parsed = listToolsResult.safeParse(raw ?? {});
|
|
161
|
+
if (!parsed.success) {
|
|
162
|
+
// A malformed page means the catalog is PARTIAL — saying otherwise would cache a truncated list
|
|
163
|
+
// as complete and silently hide the server's remaining tools.
|
|
164
|
+
this.protocolErrors.push(`tools/list page ${page + 1} was malformed; the catalog is partial`);
|
|
165
|
+
truncated = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
for (const decl of parsed.data.tools ?? []) {
|
|
169
|
+
const norm = normalizeToolDecl(decl);
|
|
170
|
+
if (!norm) {
|
|
171
|
+
rejected += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (seen.has(norm.name))
|
|
175
|
+
continue; // a server repeating a name adds nothing
|
|
176
|
+
if (tools.length >= maxTools) {
|
|
177
|
+
truncated = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
seen.add(norm.name);
|
|
181
|
+
tools.push(norm);
|
|
182
|
+
}
|
|
183
|
+
cursor = parsed.data.nextCursor;
|
|
184
|
+
if (!cursor || truncated)
|
|
185
|
+
break;
|
|
186
|
+
if (page === maxPages - 1)
|
|
187
|
+
truncated = true;
|
|
188
|
+
}
|
|
189
|
+
return { tools, rejected, truncated };
|
|
190
|
+
}
|
|
191
|
+
async callTool(name, args, opts = {}) {
|
|
192
|
+
const raw = await this.request('tools/call', { name, arguments: args ?? {} }, opts);
|
|
193
|
+
return flattenCallResult(raw ?? {});
|
|
194
|
+
}
|
|
195
|
+
async ping() {
|
|
196
|
+
const started = this.clock.now();
|
|
197
|
+
try {
|
|
198
|
+
await this.request('ping', {}, { timeoutMs: Math.min(this.opts.timeoutMs ?? 30_000, 5_000) });
|
|
199
|
+
return { ok: true, latencyMs: this.clock.now() - started };
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { ok: false, latencyMs: this.clock.now() - started };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async close() {
|
|
206
|
+
for (const [, p] of this.pending) {
|
|
207
|
+
clearTimeout(p.timer);
|
|
208
|
+
p.detach?.();
|
|
209
|
+
p.reject(new AIError(`MCP '${this.opts.serverId}' closed`, { category: 'NETWORK', retryable: true }));
|
|
210
|
+
}
|
|
211
|
+
this.pending.clear();
|
|
212
|
+
await this.transport.close();
|
|
213
|
+
}
|
|
214
|
+
diagnostics() {
|
|
215
|
+
return [...this.protocolErrors.slice(-5), ...this.transport.diagnostics()];
|
|
216
|
+
}
|
|
217
|
+
/** Whether the transport says our session is gone and only a fresh handshake can recover. */
|
|
218
|
+
sessionExpired() {
|
|
219
|
+
return this.transport.sessionExpired === true;
|
|
220
|
+
}
|
|
221
|
+
}
|