@lensagent/core 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/config/lens.example.jsonc +92 -0
- package/config/pair.example.json +3 -0
- package/dist/adapters/executor.d.ts +35 -0
- package/dist/adapters/executor.js +296 -0
- package/dist/adapters/lsp.d.ts +18 -0
- package/dist/adapters/lsp.js +218 -0
- package/dist/adapters/mcp.d.ts +32 -0
- package/dist/adapters/mcp.js +283 -0
- package/dist/adapters/revert.d.ts +12 -0
- package/dist/adapters/revert.js +155 -0
- package/dist/config/runtime.d.ts +55 -0
- package/dist/config/runtime.js +371 -0
- package/dist/config/schema.d.ts +292 -0
- package/dist/config/schema.js +177 -0
- package/dist/contracts/adapter.d.ts +28 -0
- package/dist/contracts/adapter.js +8 -0
- package/dist/contracts/capability.d.ts +41 -0
- package/dist/contracts/capability.js +1 -0
- package/dist/contracts/domain.d.ts +61 -0
- package/dist/contracts/domain.js +1 -0
- package/dist/contracts/index.d.ts +5 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/model.d.ts +77 -0
- package/dist/contracts/model.js +1 -0
- package/dist/contracts/ports.d.ts +14 -0
- package/dist/contracts/ports.js +14 -0
- package/dist/core/access.d.ts +24 -0
- package/dist/core/access.js +205 -0
- package/dist/core/codec.d.ts +33 -0
- package/dist/core/codec.js +105 -0
- package/dist/core/domain.d.ts +13 -0
- package/dist/core/domain.js +24 -0
- package/dist/core/host.d.ts +19 -0
- package/dist/core/host.js +249 -0
- package/dist/core/identity.d.ts +19 -0
- package/dist/core/identity.js +54 -0
- package/dist/entry/bun.d.ts +1 -0
- package/dist/entry/bun.js +2 -0
- package/dist/entry/node.d.ts +2 -0
- package/dist/entry/node.js +3 -0
- package/dist/entry/rebuild.d.ts +1 -0
- package/dist/entry/rebuild.js +47 -0
- package/dist/entry/run.d.ts +1 -0
- package/dist/entry/run.js +43 -0
- package/dist/execution/lsp-process.d.ts +14 -0
- package/dist/execution/lsp-process.js +234 -0
- package/dist/http/server.d.ts +34 -0
- package/dist/http/server.js +1332 -0
- package/dist/launch.d.ts +3 -0
- package/dist/launch.js +4 -0
- package/dist/runtime/agent.d.ts +35 -0
- package/dist/runtime/agent.js +853 -0
- package/dist/runtime/artifacts.d.ts +15 -0
- package/dist/runtime/artifacts.js +72 -0
- package/dist/runtime/cache.d.ts +20 -0
- package/dist/runtime/cache.js +44 -0
- package/dist/runtime/calls.d.ts +21 -0
- package/dist/runtime/calls.js +130 -0
- package/dist/runtime/capabilities.d.ts +58 -0
- package/dist/runtime/capabilities.js +196 -0
- package/dist/runtime/cleanup.d.ts +7 -0
- package/dist/runtime/cleanup.js +33 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +169 -0
- package/dist/runtime/diagnostics.d.ts +8 -0
- package/dist/runtime/diagnostics.js +71 -0
- package/dist/runtime/events.d.ts +22 -0
- package/dist/runtime/events.js +131 -0
- package/dist/runtime/kernel-capabilities.d.ts +32 -0
- package/dist/runtime/kernel-capabilities.js +200 -0
- package/dist/runtime/manifests.d.ts +14 -0
- package/dist/runtime/manifests.js +117 -0
- package/dist/runtime/model.d.ts +20 -0
- package/dist/runtime/model.js +241 -0
- package/dist/runtime/observability.d.ts +22 -0
- package/dist/runtime/observability.js +104 -0
- package/dist/runtime/policy.d.ts +8 -0
- package/dist/runtime/policy.js +31 -0
- package/dist/runtime/profile.d.ts +8 -0
- package/dist/runtime/profile.js +52 -0
- package/dist/runtime/projects.d.ts +40 -0
- package/dist/runtime/projects.js +115 -0
- package/dist/runtime/results.d.ts +7 -0
- package/dist/runtime/results.js +55 -0
- package/dist/runtime/review.d.ts +9 -0
- package/dist/runtime/review.js +54 -0
- package/dist/runtime/scheduler.d.ts +15 -0
- package/dist/runtime/scheduler.js +85 -0
- package/dist/runtime/skills.d.ts +2 -0
- package/dist/runtime/skills.js +83 -0
- package/dist/runtime/tokenizer.d.ts +8 -0
- package/dist/runtime/tokenizer.js +30 -0
- package/dist/store/database.d.ts +3 -0
- package/dist/store/database.js +49 -0
- package/dist/store/event-log.d.ts +19 -0
- package/dist/store/event-log.js +61 -0
- package/dist/store/schema.d.ts +1 -0
- package/dist/store/schema.js +347 -0
- package/dist/store/store.d.ts +344 -0
- package/dist/store/store.js +2554 -0
- package/package.json +73 -0
- package/proto/executor.proto +314 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { fail } from '../core/domain.js';
|
|
5
|
+
import { LspProcess } from '../execution/lsp-process.js';
|
|
6
|
+
import { capabilityManifest } from '../runtime/manifests.js';
|
|
7
|
+
export class LspPool {
|
|
8
|
+
servers;
|
|
9
|
+
#clients = new Map();
|
|
10
|
+
constructor(servers) {
|
|
11
|
+
this.servers = servers;
|
|
12
|
+
}
|
|
13
|
+
async request(serverId, workspacePaths, sessionId, method, params, signal) {
|
|
14
|
+
const projectPath = this.projectPath(serverId, workspacePaths);
|
|
15
|
+
assertFileUris(workspacePaths, params);
|
|
16
|
+
const selected = await this.#client(serverId, projectPath, sessionId);
|
|
17
|
+
try {
|
|
18
|
+
if (isNotification(method))
|
|
19
|
+
selected.process.notify(method, params);
|
|
20
|
+
const response = isNotification(method) ? null : jsonValue(await selected.process.request(method, params, signal));
|
|
21
|
+
return { method, response, notifications: selected.process.drainNotifications().map(jsonValue), restarted: selected.restarted };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (selected.process.closed)
|
|
25
|
+
this.#clients.delete(key(serverId, projectPath, sessionId));
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
projectPath(serverId, workspacePaths) {
|
|
30
|
+
const configured = this.servers[serverId]?.project ?? '.';
|
|
31
|
+
const roots = workspacePaths.map((path) => realpathSync(resolve(path)));
|
|
32
|
+
const root = isAbsolute(configured) ? roots.find((candidate) => within(candidate, configured)) : roots[0];
|
|
33
|
+
if (root === undefined)
|
|
34
|
+
fail('WORKSPACE_INVALID', 400, 'The LSP project is outside the Workspace.');
|
|
35
|
+
const project = resolve(root, configured);
|
|
36
|
+
assertRealPath(root, project);
|
|
37
|
+
return project;
|
|
38
|
+
}
|
|
39
|
+
async close() {
|
|
40
|
+
await Promise.allSettled([...this.#clients.values()].map(async (client) => (await client).close()));
|
|
41
|
+
this.#clients.clear();
|
|
42
|
+
}
|
|
43
|
+
async #client(serverId, workspacePath, sessionId) {
|
|
44
|
+
const sessionKey = key(serverId, workspacePath, sessionId);
|
|
45
|
+
const existing = this.#clients.get(sessionKey);
|
|
46
|
+
if (existing !== undefined) {
|
|
47
|
+
const process = await existing;
|
|
48
|
+
if (!process.closed)
|
|
49
|
+
return { process, restarted: false };
|
|
50
|
+
this.#clients.delete(sessionKey);
|
|
51
|
+
}
|
|
52
|
+
const config = this.servers[serverId];
|
|
53
|
+
if (config === undefined)
|
|
54
|
+
fail('LSP_SERVER_UNAVAILABLE', 404, `LSP server ${serverId} is not configured.`);
|
|
55
|
+
const created = Promise.resolve().then(async () => {
|
|
56
|
+
const process = new LspProcess(config.command, config.args ?? [], workspacePath, config.maxFrameBytes ?? 4194304, config.maxBufferBytes ?? 8388608);
|
|
57
|
+
await process.request('initialize', {
|
|
58
|
+
processId: process.pid ?? null,
|
|
59
|
+
rootUri: pathToFileURL(workspacePath).href,
|
|
60
|
+
capabilities: {},
|
|
61
|
+
initializationOptions: config.initializationOptions ?? null
|
|
62
|
+
}, AbortSignal.timeout(10000));
|
|
63
|
+
process.markInitialized();
|
|
64
|
+
process.notify('initialized', {});
|
|
65
|
+
return process;
|
|
66
|
+
});
|
|
67
|
+
this.#clients.set(sessionKey, created);
|
|
68
|
+
try {
|
|
69
|
+
return { process: await created, restarted: true };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.#clients.delete(sessionKey);
|
|
73
|
+
void created.then((process) => process.close(), () => undefined);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class LspAdapter {
|
|
79
|
+
pool;
|
|
80
|
+
id = 'lsp';
|
|
81
|
+
constructor(pool) {
|
|
82
|
+
this.pool = pool;
|
|
83
|
+
}
|
|
84
|
+
async execute(invocation) {
|
|
85
|
+
const input = object(invocation.arguments);
|
|
86
|
+
const serverId = string(input.server_id);
|
|
87
|
+
const method = string(input.method);
|
|
88
|
+
if (!allowedMethods.has(method))
|
|
89
|
+
fail('CALL_ARGUMENTS_INVALID', 422, `LSP method ${method} is not allowed.`);
|
|
90
|
+
if (invocation.workspacePaths.length === 0)
|
|
91
|
+
fail('WORKSPACE_INVALID', 503, 'LSP requires a Workspace root.');
|
|
92
|
+
return { content: await this.pool.request(serverId, invocation.workspacePaths, optionalString(input.session_id) ?? 'default', method, jsonValue(input.params ?? {}), invocation.signal) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function lspCapabilities(config, pool) {
|
|
96
|
+
const serverIds = Object.keys(config).sort();
|
|
97
|
+
if (serverIds.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const adapter = new LspAdapter(pool);
|
|
100
|
+
const definition = {
|
|
101
|
+
id: 'lsp_request',
|
|
102
|
+
title: 'LSP request',
|
|
103
|
+
description: 'Send an allowed request or notification to a configured Language Server.',
|
|
104
|
+
adapterId: adapter.id,
|
|
105
|
+
effect: 'read_only',
|
|
106
|
+
loadMode: 'deferred',
|
|
107
|
+
outputLimitBytes: 1048576,
|
|
108
|
+
defaultAssignment: 'configured',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
server_id: { type: 'string', enum: serverIds },
|
|
114
|
+
session_id: { type: 'string', minLength: 1 },
|
|
115
|
+
method: { type: 'string', enum: [...allowedMethods] },
|
|
116
|
+
params: { type: 'object', additionalProperties: true }
|
|
117
|
+
},
|
|
118
|
+
required: ['server_id', 'method']
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return [{
|
|
122
|
+
definition,
|
|
123
|
+
adapter,
|
|
124
|
+
manifest: capabilityManifest(definition, ['read_only', 'workspace_write']),
|
|
125
|
+
effect: (input) => writeMethods.has(string(object(input).method)) ? 'workspace_write' : 'read_only',
|
|
126
|
+
claims: (context) => [{ resource: context.workspacePaths[0] ?? '*', mode: writeMethods.has(string(object(context.arguments).method)) ? 'write' : 'read' }]
|
|
127
|
+
}];
|
|
128
|
+
}
|
|
129
|
+
const allowedMethods = new Set([
|
|
130
|
+
'textDocument/didOpen',
|
|
131
|
+
'textDocument/didChange',
|
|
132
|
+
'textDocument/didClose',
|
|
133
|
+
'textDocument/didSave',
|
|
134
|
+
'textDocument/diagnostic',
|
|
135
|
+
'textDocument/hover',
|
|
136
|
+
'textDocument/definition',
|
|
137
|
+
'textDocument/references',
|
|
138
|
+
'textDocument/rename',
|
|
139
|
+
'textDocument/codeAction',
|
|
140
|
+
'textDocument/completion',
|
|
141
|
+
'project/diagnostic',
|
|
142
|
+
'project/symbol',
|
|
143
|
+
'workspace/symbol'
|
|
144
|
+
]);
|
|
145
|
+
const writeMethods = new Set(['textDocument/rename', 'textDocument/codeAction']);
|
|
146
|
+
function isNotification(method) {
|
|
147
|
+
return method.startsWith('textDocument/did');
|
|
148
|
+
}
|
|
149
|
+
function assertFileUris(workspacePaths, value) {
|
|
150
|
+
if (typeof value === 'string' && value.toLowerCase().startsWith('file:')) {
|
|
151
|
+
let path;
|
|
152
|
+
try {
|
|
153
|
+
path = fileURLToPath(value);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
fail('CALL_ARGUMENTS_INVALID', 422, 'The LSP request contains an invalid file URI.');
|
|
157
|
+
}
|
|
158
|
+
const roots = workspacePaths.map((root) => realpathSync(resolve(root)));
|
|
159
|
+
const root = roots.find((candidate) => within(candidate, path));
|
|
160
|
+
if (root === undefined)
|
|
161
|
+
fail('CALL_ARGUMENTS_INVALID', 422, 'The LSP request references a file outside the Workspace.');
|
|
162
|
+
assertRealPath(root, path);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
for (const entry of value)
|
|
167
|
+
assertFileUris(workspacePaths, entry);
|
|
168
|
+
}
|
|
169
|
+
else if (objectLike(value)) {
|
|
170
|
+
for (const entry of Object.values(value))
|
|
171
|
+
assertFileUris(workspacePaths, entry);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function assertRealPath(root, target) {
|
|
175
|
+
if (!within(root, target))
|
|
176
|
+
fail('CALL_ARGUMENTS_INVALID', 422, 'The LSP path is outside the Workspace.');
|
|
177
|
+
let existing = target;
|
|
178
|
+
while (!existsSync(existing)) {
|
|
179
|
+
const parent = dirname(existing);
|
|
180
|
+
if (parent === existing)
|
|
181
|
+
break;
|
|
182
|
+
existing = parent;
|
|
183
|
+
}
|
|
184
|
+
if (!within(root, realpathSync(existing)))
|
|
185
|
+
fail('CALL_ARGUMENTS_INVALID', 422, 'The LSP path resolves outside the Workspace.');
|
|
186
|
+
}
|
|
187
|
+
function within(root, target) {
|
|
188
|
+
const relation = relative(root, resolve(target));
|
|
189
|
+
return relation === '' || (relation !== '..' && !relation.startsWith(`..${sep}`) && !isAbsolute(relation));
|
|
190
|
+
}
|
|
191
|
+
function key(serverId, workspacePath, sessionId) {
|
|
192
|
+
return `${serverId}\u0000${workspacePath}\u0000${sessionId}`;
|
|
193
|
+
}
|
|
194
|
+
function object(value) {
|
|
195
|
+
if (!objectLike(value))
|
|
196
|
+
fail('INVALID_INPUT', 400, 'LSP input must be an object.');
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function objectLike(value) {
|
|
200
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
201
|
+
}
|
|
202
|
+
function string(value) {
|
|
203
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
204
|
+
fail('INVALID_INPUT', 400, 'A required LSP string is missing.');
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function optionalString(value) {
|
|
208
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
209
|
+
}
|
|
210
|
+
function jsonValue(value) {
|
|
211
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
|
|
212
|
+
return value;
|
|
213
|
+
if (Array.isArray(value))
|
|
214
|
+
return value.map(jsonValue);
|
|
215
|
+
if (typeof value === 'object')
|
|
216
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsonValue(entry)]));
|
|
217
|
+
fail('LSP_PROTOCOL_ERROR', 502, 'LSP returned a non-JSON value.');
|
|
218
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CapabilityAdapter, type CapabilityInvocation, type CapabilityOutput, type Json } from '../core/domain.js';
|
|
2
|
+
import type { HarnessConfig } from '../config/schema.js';
|
|
3
|
+
import type { RegisteredCapability } from '../runtime/capabilities.js';
|
|
4
|
+
interface McpTool {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly title?: string;
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
readonly inputSchema: Readonly<Record<string, Json>>;
|
|
9
|
+
readonly outputSchema?: Readonly<Record<string, Json>>;
|
|
10
|
+
}
|
|
11
|
+
export declare class McpElicitationError extends Error {
|
|
12
|
+
readonly prompt: Json;
|
|
13
|
+
constructor(prompt: Json);
|
|
14
|
+
}
|
|
15
|
+
export declare class McpClient {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly config: NonNullable<HarnessConfig['mcpServers']>[string];
|
|
18
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
19
|
+
constructor(id: string, config: NonNullable<HarnessConfig['mcpServers']>[string]);
|
|
20
|
+
discover(signal?: AbortSignal): Promise<readonly McpTool[]>;
|
|
21
|
+
call(name: string, argumentsValue: Json, answer: Json | undefined, requestState: string | undefined, signal: AbortSignal): Promise<Json>;
|
|
22
|
+
request(method: string, params: Json, signal?: AbortSignal): Promise<Json>;
|
|
23
|
+
}
|
|
24
|
+
export declare class McpAdapter implements CapabilityAdapter {
|
|
25
|
+
readonly serverId: string;
|
|
26
|
+
readonly client: McpClient;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
constructor(serverId: string, client: McpClient);
|
|
29
|
+
execute(invocation: CapabilityInvocation): Promise<CapabilityOutput>;
|
|
30
|
+
}
|
|
31
|
+
export declare function mcpCapabilities(serverId: string, config: NonNullable<HarnessConfig['mcpServers']>[string]): Promise<readonly RegisteredCapability[]>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { fail } from '../core/domain.js';
|
|
3
|
+
import { capabilityManifest } from '../runtime/manifests.js';
|
|
4
|
+
import { componentIdentity } from '../core/identity.js';
|
|
5
|
+
const protocolVersion = '2026-07-28';
|
|
6
|
+
export class McpElicitationError extends Error {
|
|
7
|
+
prompt;
|
|
8
|
+
constructor(prompt) {
|
|
9
|
+
super('MCP server requires user input.');
|
|
10
|
+
this.prompt = prompt;
|
|
11
|
+
this.name = 'McpElicitationError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class McpClient {
|
|
15
|
+
id;
|
|
16
|
+
config;
|
|
17
|
+
headers;
|
|
18
|
+
constructor(id, config) {
|
|
19
|
+
this.id = id;
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.headers = resolveHeaders(id, config.headers ?? {});
|
|
22
|
+
}
|
|
23
|
+
async discover(signal) {
|
|
24
|
+
const tools = [];
|
|
25
|
+
const cursors = new Set();
|
|
26
|
+
let cursor;
|
|
27
|
+
do {
|
|
28
|
+
const result = object(await this.request('tools/list', cursor === undefined ? {} : { cursor }, signal));
|
|
29
|
+
if (!Array.isArray(result.tools))
|
|
30
|
+
fail('MCP_PROTOCOL_ERROR', 502, `MCP server ${this.id} returned an invalid tools/list result.`);
|
|
31
|
+
tools.push(...result.tools.map(tool));
|
|
32
|
+
if (result.nextCursor === undefined)
|
|
33
|
+
cursor = undefined;
|
|
34
|
+
else {
|
|
35
|
+
cursor = string(result.nextCursor);
|
|
36
|
+
if (cursors.has(cursor))
|
|
37
|
+
fail('MCP_PROTOCOL_ERROR', 502, `MCP server ${this.id} repeated a tools/list cursor.`);
|
|
38
|
+
cursors.add(cursor);
|
|
39
|
+
}
|
|
40
|
+
} while (cursor !== undefined);
|
|
41
|
+
return tools.sort((left, right) => left.name.localeCompare(right.name));
|
|
42
|
+
}
|
|
43
|
+
async call(name, argumentsValue, answer, requestState, signal) {
|
|
44
|
+
const result = object(await this.request('tools/call', {
|
|
45
|
+
name,
|
|
46
|
+
arguments: argumentsValue,
|
|
47
|
+
...(answer === undefined ? {} : { inputResponses: answer }),
|
|
48
|
+
...(requestState === undefined ? {} : { requestState })
|
|
49
|
+
}, signal));
|
|
50
|
+
if (result.resultType === 'input_required') {
|
|
51
|
+
const inputRequests = result.inputRequests === undefined ? {} : object(result.inputRequests);
|
|
52
|
+
const state = result.requestState === undefined ? undefined : string(result.requestState);
|
|
53
|
+
throw new McpElicitationError({
|
|
54
|
+
kind: 'mcp_input_required',
|
|
55
|
+
input_requests: jsonValue(inputRequests),
|
|
56
|
+
...(state === undefined ? {} : { request_state: state })
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return jsonValue(result);
|
|
60
|
+
}
|
|
61
|
+
async request(method, params, signal) {
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
65
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(new Error('MCP request timed out.')), this.config.timeoutMs ?? 30000);
|
|
67
|
+
try {
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(this.config.endpoint, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
headers: {
|
|
74
|
+
...this.headers,
|
|
75
|
+
'content-type': 'application/json',
|
|
76
|
+
accept: 'application/json, text/event-stream',
|
|
77
|
+
'mcp-protocol-version': protocolVersion
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
jsonrpc: '2.0',
|
|
81
|
+
id,
|
|
82
|
+
method,
|
|
83
|
+
params: {
|
|
84
|
+
...object(params),
|
|
85
|
+
_meta: {
|
|
86
|
+
'io.modelcontextprotocol/protocolVersion': protocolVersion,
|
|
87
|
+
// Read from the installed package metadata rather than repeated here. A literal
|
|
88
|
+
// was pinned to 0.1.0 and told every MCP server the wrong version from the first
|
|
89
|
+
// release onwards, because nothing makes a hand-copied version follow a bump.
|
|
90
|
+
'io.modelcontextprotocol/clientInfo': { name: 'Lens', version: componentIdentity().softwareVersion },
|
|
91
|
+
'io.modelcontextprotocol/clientCapabilities': { elicitation: { form: {}, url: {} } }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (signal?.aborted === true)
|
|
99
|
+
throw signal.reason;
|
|
100
|
+
fail('MCP_SERVER_UNAVAILABLE', 502, `MCP server ${this.id} could not be reached.`, { cause: error instanceof Error ? error.message : String(error) });
|
|
101
|
+
}
|
|
102
|
+
const source = await boundedText(response, this.config.outputLimitBytes ?? 1048576);
|
|
103
|
+
const envelopes = response.headers.get('content-type')?.includes('text/event-stream') === true
|
|
104
|
+
? sseData(source).map(parseEnvelope)
|
|
105
|
+
: [parseEnvelope(source)];
|
|
106
|
+
const document = envelopes.find((entry) => entry.id === id);
|
|
107
|
+
if (document === undefined || document.jsonrpc !== '2.0')
|
|
108
|
+
fail('MCP_PROTOCOL_ERROR', 502, `MCP server ${this.id} returned an invalid JSON-RPC response.`);
|
|
109
|
+
if (document.error !== undefined)
|
|
110
|
+
fail('MCP_PROTOCOL_ERROR', 502, `MCP server ${this.id} returned a JSON-RPC error.`, { error: jsonValue(document.error) });
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
fail('MCP_SERVER_UNAVAILABLE', 502, `MCP server ${this.id} returned HTTP ${response.status}.`);
|
|
113
|
+
if (!Object.hasOwn(document, 'result'))
|
|
114
|
+
fail('MCP_PROTOCOL_ERROR', 502, `MCP server ${this.id} returned no result.`);
|
|
115
|
+
return jsonValue(document.result);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
signal?.removeEventListener('abort', onAbort);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export class McpAdapter {
|
|
124
|
+
serverId;
|
|
125
|
+
client;
|
|
126
|
+
id;
|
|
127
|
+
constructor(serverId, client) {
|
|
128
|
+
this.serverId = serverId;
|
|
129
|
+
this.client = client;
|
|
130
|
+
this.id = `mcp:${serverId}`;
|
|
131
|
+
}
|
|
132
|
+
async execute(invocation) {
|
|
133
|
+
const separator = invocation.capabilityId.indexOf('/', 4);
|
|
134
|
+
if (separator < 0)
|
|
135
|
+
fail('CAPABILITY_NOT_FOUND', 404, 'The MCP Capability ID is invalid.');
|
|
136
|
+
const input = object(invocation.arguments);
|
|
137
|
+
const argumentsValue = input.arguments === undefined ? invocation.arguments : jsonValue(input.arguments);
|
|
138
|
+
const answer = input.answer === undefined ? undefined : jsonValue(input.answer);
|
|
139
|
+
const prompt = input.prompt === undefined ? undefined : object(input.prompt);
|
|
140
|
+
const requestState = prompt?.request_state === undefined ? undefined : string(prompt.request_state);
|
|
141
|
+
return output(await this.client.call(invocation.capabilityId.slice(separator + 1), argumentsValue, answer, requestState, invocation.signal));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function mcpCapabilities(serverId, config) {
|
|
145
|
+
const client = new McpClient(serverId, config);
|
|
146
|
+
const adapter = new McpAdapter(serverId, client);
|
|
147
|
+
const definitions = new Map();
|
|
148
|
+
for (const entry of await client.discover()) {
|
|
149
|
+
const id = `mcp:${serverId}/${entry.name}`;
|
|
150
|
+
if (definitions.has(id))
|
|
151
|
+
continue;
|
|
152
|
+
definitions.set(id, {
|
|
153
|
+
id,
|
|
154
|
+
title: entry.title ?? entry.name,
|
|
155
|
+
description: entry.description ?? `MCP tool ${entry.name} from ${serverId}.`,
|
|
156
|
+
inputSchema: entry.inputSchema,
|
|
157
|
+
...(entry.outputSchema === undefined ? {} : { outputSchema: entry.outputSchema }),
|
|
158
|
+
effect: 'full',
|
|
159
|
+
adapterId: adapter.id,
|
|
160
|
+
loadMode: 'deferred',
|
|
161
|
+
outputLimitBytes: config.outputLimitBytes ?? 1048576,
|
|
162
|
+
defaultAssignment: 'configured'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return [...definitions.values()].map((definition) => ({
|
|
166
|
+
definition,
|
|
167
|
+
adapter,
|
|
168
|
+
manifest: capabilityManifest(definition),
|
|
169
|
+
claims: () => [{ resource: `mcp:${serverId}`, mode: 'write' }]
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
function tool(value) {
|
|
173
|
+
const source = object(value);
|
|
174
|
+
return {
|
|
175
|
+
name: string(source.name),
|
|
176
|
+
...(typeof source.title === 'string' ? { title: source.title } : {}),
|
|
177
|
+
...(typeof source.description === 'string' ? { description: source.description } : {}),
|
|
178
|
+
inputSchema: schema(source.inputSchema),
|
|
179
|
+
...(source.outputSchema === undefined ? {} : { outputSchema: schema(source.outputSchema) })
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function output(value) {
|
|
183
|
+
const result = object(value);
|
|
184
|
+
if (!Array.isArray(result.content))
|
|
185
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP tools/call returned invalid content.');
|
|
186
|
+
const text = [];
|
|
187
|
+
const links = [];
|
|
188
|
+
const artifacts = [];
|
|
189
|
+
for (const [index, candidate] of result.content.entries()) {
|
|
190
|
+
const block = object(candidate);
|
|
191
|
+
if (block.type === 'text')
|
|
192
|
+
text.push(string(block.text));
|
|
193
|
+
else if (block.type === 'image' || block.type === 'audio')
|
|
194
|
+
artifacts.push(binaryArtifact(block, `mcp-${block.type}-${index}`));
|
|
195
|
+
else if (block.type === 'resource_link')
|
|
196
|
+
links.push(jsonValue(block));
|
|
197
|
+
else if (block.type === 'resource') {
|
|
198
|
+
const resource = object(block.resource);
|
|
199
|
+
if (typeof resource.text === 'string')
|
|
200
|
+
text.push(resource.text);
|
|
201
|
+
else if (typeof resource.blob === 'string')
|
|
202
|
+
artifacts.push(binaryArtifact(resource, `mcp-resource-${index}`));
|
|
203
|
+
else
|
|
204
|
+
links.push(jsonValue(resource));
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP tools/call returned an unsupported content block.');
|
|
208
|
+
}
|
|
209
|
+
const content = {
|
|
210
|
+
text: text.join('\n'),
|
|
211
|
+
...(result.structuredContent === undefined ? {} : { structured_content: jsonValue(result.structuredContent) }),
|
|
212
|
+
...(links.length === 0 ? {} : { resource_links: links })
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
content,
|
|
216
|
+
...(artifacts.length === 0 ? {} : { artifacts }),
|
|
217
|
+
...(result.isError === true ? { outcome: 'failed', errorCode: 'MCP_TOOL_EXECUTION_FAILED' } : {})
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function binaryArtifact(value, name) {
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
mediaType: typeof value.mimeType === 'string' ? value.mimeType : 'application/octet-stream',
|
|
224
|
+
bytes: Buffer.from(string(value.data ?? value.blob), 'base64')
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function resolveHeaders(serverId, headers) {
|
|
228
|
+
const result = {};
|
|
229
|
+
for (const [name, reference] of Object.entries(headers)) {
|
|
230
|
+
if (typeof reference === 'string')
|
|
231
|
+
result[name] = reference;
|
|
232
|
+
else {
|
|
233
|
+
const value = process.env[reference.env];
|
|
234
|
+
if (value !== undefined && value.length > 0)
|
|
235
|
+
result[name] = value;
|
|
236
|
+
else if (reference.optional !== true)
|
|
237
|
+
fail('MCP_SERVER_UNAVAILABLE', 503, `MCP server ${serverId} requires environment variable ${reference.env}.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
async function boundedText(response, limit) {
|
|
243
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
244
|
+
if (bytes.byteLength > limit)
|
|
245
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP response exceeded its configured limit.');
|
|
246
|
+
return bytes.toString();
|
|
247
|
+
}
|
|
248
|
+
function sseData(source) {
|
|
249
|
+
return source.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n\n').flatMap((block) => {
|
|
250
|
+
const data = block.split('\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart());
|
|
251
|
+
return data.length === 0 ? [] : [data.join('\n')];
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function parseEnvelope(source) {
|
|
255
|
+
try {
|
|
256
|
+
return object(JSON.parse(source));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP returned invalid JSON.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function object(value) {
|
|
263
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
264
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP returned an invalid object.');
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
function schema(value) {
|
|
268
|
+
return object(jsonValue(value));
|
|
269
|
+
}
|
|
270
|
+
function string(value) {
|
|
271
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
272
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP returned an invalid string.');
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
function jsonValue(value) {
|
|
276
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
|
|
277
|
+
return value;
|
|
278
|
+
if (Array.isArray(value))
|
|
279
|
+
return value.map(jsonValue);
|
|
280
|
+
if (typeof value === 'object')
|
|
281
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsonValue(entry)]));
|
|
282
|
+
fail('MCP_PROTOCOL_ERROR', 502, 'MCP returned a non-JSON value.');
|
|
283
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CapabilityAdapter, type CapabilityInvocation, type CapabilityOutput } from '../core/domain.js';
|
|
2
|
+
import type { RegisteredCapability } from '../runtime/capabilities.js';
|
|
3
|
+
import type { Store } from '../store/store.js';
|
|
4
|
+
import { type ExecutorClient } from './executor.js';
|
|
5
|
+
export declare class RevertAdapter implements CapabilityAdapter {
|
|
6
|
+
readonly store: Store;
|
|
7
|
+
readonly executor: ExecutorClient;
|
|
8
|
+
readonly id = "revert";
|
|
9
|
+
constructor(store: Store, executor: ExecutorClient);
|
|
10
|
+
execute(invocation: CapabilityInvocation): Promise<CapabilityOutput>;
|
|
11
|
+
}
|
|
12
|
+
export declare function revertCapabilities(store: Store, executor: ExecutorClient): readonly RegisteredCapability[];
|