@dev-mainsequence/codex-plugin 0.0.0-stage → 0.4.2

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 (57) hide show
  1. package/.codex-plugin/plugin.json +22 -0
  2. package/.mcp.json +16 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +13 -0
  5. package/README.md +155 -2
  6. package/assets/mainsequence-icon.png +0 -0
  7. package/assets/mainsequence-icon.svg +454 -0
  8. package/build-info.json +10 -0
  9. package/dist/hosts/codex.js +62 -0
  10. package/dist/hosts/codexPresentation.js +165 -0
  11. package/dist/hosts/discovery.js +138 -0
  12. package/dist/hosts/panel/panel.html +165 -0
  13. package/dist/package.json +1 -0
  14. package/dist/panel-server.js +135 -0
  15. package/dist/runner.js +71 -0
  16. package/dist/vendor/devtools/LICENSE +21 -0
  17. package/dist/vendor/devtools/NOTICE +12 -0
  18. package/dist/vendor/devtools/index.js +72 -0
  19. package/dist/vendor/devtools/package.json +5 -0
  20. package/dist/vendor/devtools/shared/accountScope.js +19 -0
  21. package/dist/vendor/devtools/shared/actions.js +164 -0
  22. package/dist/vendor/devtools/shared/agentReadiness.js +71 -0
  23. package/dist/vendor/devtools/shared/agents.js +240 -0
  24. package/dist/vendor/devtools/shared/arguments.js +142 -0
  25. package/dist/vendor/devtools/shared/cli.js +144 -0
  26. package/dist/vendor/devtools/shared/config.js +64 -0
  27. package/dist/vendor/devtools/shared/devcontainerJsonc.js +153 -0
  28. package/dist/vendor/devtools/shared/envfile.js +128 -0
  29. package/dist/vendor/devtools/shared/environments.js +38 -0
  30. package/dist/vendor/devtools/shared/hostSession.js +324 -0
  31. package/dist/vendor/devtools/shared/machineSession.js +338 -0
  32. package/dist/vendor/devtools/shared/mcp.js +421 -0
  33. package/dist/vendor/devtools/shared/mcpTransport.js +177 -0
  34. package/dist/vendor/devtools/shared/models.js +12 -0
  35. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +300 -0
  36. package/dist/vendor/devtools/shared/organizationProjects.js +134 -0
  37. package/dist/vendor/devtools/shared/process.js +61 -0
  38. package/dist/vendor/devtools/shared/projectOperations.js +279 -0
  39. package/dist/vendor/devtools/shared/projects.js +390 -0
  40. package/dist/vendor/devtools/shared/pythonProjectStatus.js +86 -0
  41. package/dist/vendor/devtools/shared/pythonSdkStatus.js +26 -0
  42. package/dist/vendor/devtools/shared/repositories.js +215 -0
  43. package/dist/vendor/devtools/shared/repositoryClient.js +108 -0
  44. package/dist/vendor/devtools/shared/repositoryIdentity.js +116 -0
  45. package/dist/vendor/devtools/shared/repositoryModels.js +72 -0
  46. package/dist/vendor/devtools/shared/session.js +532 -0
  47. package/docs/adr/0002-conversational-project-mcp-client.md +59 -0
  48. package/docs/agents-tools-panel.md +63 -0
  49. package/docs/codex-verification.md +161 -0
  50. package/docs/conversational-mcp.md +54 -0
  51. package/hooks/hooks.json +17 -0
  52. package/package.json +50 -4
  53. package/skills/mainsequence-mcp/SKILL.md +40 -0
  54. package/skills/ms/SKILL.md +8 -0
  55. package/skills/ms/agents/openai.yaml +7 -0
  56. package/skills/ms/assets/mainsequence-icon.png +0 -0
  57. package/skills/ms/assets/mainsequence-icon.svg +454 -0
@@ -0,0 +1,421 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.McpConnections = void 0;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const path = __importStar(require("node:path"));
39
+ const os = __importStar(require("node:os"));
40
+ const node_crypto_1 = require("node:crypto");
41
+ const models_1 = require("./models.js");
42
+ const session_1 = require("./session.js");
43
+ const mcpTransport_1 = require("./mcpTransport.js");
44
+ const repositoryModels_1 = require("./repositoryModels.js");
45
+ const environments_1 = require("./environments.js");
46
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
47
+ function uuid(value) {
48
+ if (typeof value !== 'string' || !UUID.test(value)) {
49
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Select an exact resource release UUID from MCP discovery or saved connections.');
50
+ }
51
+ return value.toLowerCase();
52
+ }
53
+ /** Optional labels: an absent or malformed value is dropped, never an error. */
54
+ function labels(row) {
55
+ const text = (value) => typeof value === 'string' && value.trim() && !/[\r\n\0]/.test(value) ? value.trim() : undefined;
56
+ const name = text(row?.name), display = text(row?.display_name);
57
+ const branch = typeof row?.code_repository_branch_uid === 'string' && UUID.test(row.code_repository_branch_uid) ? row.code_repository_branch_uid.toLowerCase() : undefined;
58
+ return { ...(name ? { name } : {}), ...(display ? { display_name: display } : {}), ...(branch ? { code_repository_branch_uid: branch } : {}) };
59
+ }
60
+ function endpoint(value, metadata = false) {
61
+ if (typeof value !== 'string')
62
+ throw new models_1.ToolingError('API_ERROR', 'The backend returned no MCP endpoint.');
63
+ let url;
64
+ try {
65
+ url = new URL(value);
66
+ }
67
+ catch {
68
+ throw new models_1.ToolingError('API_ERROR', 'The backend returned an invalid MCP endpoint.');
69
+ }
70
+ if (url.username || url.password || (!metadata && url.search) || url.hash || /[\r\n\0]/.test(value)
71
+ || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) {
72
+ throw new models_1.ToolingError('API_ERROR', 'An HTTPS endpoint, or loopback HTTP endpoint for local development, is required.');
73
+ }
74
+ return value;
75
+ }
76
+ function connection(backend, row) {
77
+ const c = row?.mcp_connection;
78
+ if (!c || c.transport !== 'streamable-http') {
79
+ throw new models_1.ToolingError('API_ERROR', 'The selected release has no active platform MCP connection.');
80
+ }
81
+ const url = endpoint(c.url);
82
+ if (new URL(url).pathname !== '/mcp')
83
+ throw new models_1.ToolingError('API_ERROR', 'The release returned a non-canonical MCP endpoint.');
84
+ const releaseUid = uuid(row.resource_release_uid ?? row.uid), metadataUrl = endpoint(c.resource_metadata_url, true);
85
+ const metadataQuery = new URL(metadataUrl).searchParams;
86
+ if ([...metadataQuery.keys()].some(key => key !== 'resource_release_uid') || metadataQuery.getAll('resource_release_uid').length !== 1
87
+ || metadataQuery.get('resource_release_uid') !== releaseUid)
88
+ throw new models_1.ToolingError('API_ERROR', 'The MCP metadata targets another release.');
89
+ return { backend: (0, session_1.requireBackend)(backend), resource_release_uid: releaseUid, ...labels(row),
90
+ mcp_connection: { url, transport: 'streamable-http', resource_metadata_url: metadataUrl,
91
+ organization_environment_uid: uuid(c.organization_environment_uid),
92
+ ...(typeof c.organization_environment_name === 'string' && c.organization_environment_name.trim() ? { organization_environment_name: c.organization_environment_name.trim() } : {}), active_revision_uid: uuid(c.active_revision_uid) } };
93
+ }
94
+ function storagePath(env) {
95
+ const dir = process.platform === 'darwin' ? path.join(os.homedir(), 'Library', 'Application Support', 'MainSequenceCLI')
96
+ : process.platform === 'win32' ? path.join(env.APPDATA || os.homedir(), 'MainSequenceCLI') : path.join(os.homedir(), '.config', 'mainsequence');
97
+ return path.join(dir, 'mcp-connections.json');
98
+ }
99
+ /** Only endpoint metadata is persisted. Release credentials exist in one invocation's memory. */
100
+ class McpConnections {
101
+ session;
102
+ options;
103
+ file;
104
+ secrets = new Set();
105
+ constructor(session, options = {}) {
106
+ this.session = session;
107
+ this.options = options;
108
+ this.file = options.connectionsPath ?? storagePath(options.env ?? process.env);
109
+ }
110
+ sanitize(value) {
111
+ if (typeof value === 'string') {
112
+ for (const secret of this.secrets)
113
+ value = value.split(secret).join('[redacted]');
114
+ return this.session.sanitize?.(value) ?? value;
115
+ }
116
+ if (Array.isArray(value))
117
+ return value.map(item => this.sanitize(item));
118
+ if (value && typeof value === 'object')
119
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [this.sanitize(key), this.sanitize(item)]));
120
+ return value;
121
+ }
122
+ async read() {
123
+ try {
124
+ const stat = await fs.lstat(this.file);
125
+ if (!stat.isFile() || stat.size > 2_000_000)
126
+ throw new Error('invalid store');
127
+ const rows = JSON.parse(await fs.readFile(this.file, 'utf8'));
128
+ if (!Array.isArray(rows))
129
+ throw new Error('invalid store');
130
+ return rows.map(row => connection(row.backend, row));
131
+ }
132
+ catch (error) {
133
+ if (error.code === 'ENOENT')
134
+ return [];
135
+ throw new models_1.ToolingError('STORE_ERROR', 'Saved MCP connections could not be read. The existing file has been kept.');
136
+ }
137
+ }
138
+ async update(change) {
139
+ this.options.signal?.throwIfAborted();
140
+ await this.options.checkAccount?.();
141
+ await fs.mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 });
142
+ const lock = this.file + '.lock', temporary = this.file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
143
+ let handle;
144
+ try {
145
+ handle = await fs.open(lock, 'wx', 0o600);
146
+ }
147
+ catch {
148
+ throw new models_1.ToolingError('PROJECT_BUSY', 'Another action holds the saved MCP connection lock. After an interrupted action, verify it has stopped before removing ' + lock + '.');
149
+ }
150
+ try {
151
+ await handle.writeFile(JSON.stringify({ pid: process.pid }));
152
+ const rows = change(await this.read());
153
+ this.options.signal?.throwIfAborted();
154
+ const serialized = JSON.stringify(rows, null, 2) + '\n';
155
+ if (Buffer.byteLength(serialized) > 2_000_000)
156
+ throw new models_1.ToolingError('PERSISTENCE_FAILED', 'The saved MCP connections exceed the client limit.');
157
+ await fs.writeFile(temporary, serialized, { flag: 'wx', mode: 0o600 });
158
+ await fs.rename(temporary, this.file);
159
+ }
160
+ catch (error) {
161
+ if (error instanceof models_1.ToolingError)
162
+ throw error;
163
+ throw new models_1.ToolingError('PERSISTENCE_FAILED', 'The MCP connections could not be saved.');
164
+ }
165
+ finally {
166
+ await fs.rm(temporary, { force: true });
167
+ await handle.close();
168
+ await fs.unlink(lock);
169
+ }
170
+ }
171
+ async current(backend, releaseUid) {
172
+ const row = await this.session.request(backend, 'GET', '/api/v1/resource-releases/' + releaseUid + '/');
173
+ const value = connection(backend, row);
174
+ if (value.resource_release_uid !== releaseUid)
175
+ throw new models_1.ToolingError('API_ERROR', 'The backend returned a different release.');
176
+ return value;
177
+ }
178
+ async open(value) {
179
+ const grant = await this.session.request(value.backend, 'GET', '/api/v1/resource-releases/' + value.resource_release_uid + '/exchange-launch/');
180
+ if (grant?.mode !== 'token' || grant.release_kind !== 'fastapi' || typeof grant.token !== 'string' || !grant.token || /[\r\n]/.test(grant.token)) {
181
+ throw new models_1.ToolingError('API_ERROR', 'The backend returned no usable FastAPI release credential.');
182
+ }
183
+ this.secrets.add(grant.token);
184
+ if (new URL(endpoint(grant.rpc_url)).origin !== new URL(value.mcp_connection.url).origin) {
185
+ throw new models_1.ToolingError('API_ERROR', 'The release credential targets a different endpoint.');
186
+ }
187
+ const client = new mcpTransport_1.McpTransport(async (message) => {
188
+ await this.options.checkAccount?.();
189
+ const result = await (0, mcpTransport_1.sendProjectMcp)(value.mcp_connection.url, grant.token, message, this.options);
190
+ await this.options.checkAccount?.();
191
+ return result;
192
+ });
193
+ await client.initialize();
194
+ return client;
195
+ }
196
+ /** The release's name and repository from the release directory and its branch. Whatever
197
+ * cannot be read is left out: labels never make a working MCP unavailable. */
198
+ async resolveLabels(value) {
199
+ const found = {};
200
+ const read = async (apiPath) => {
201
+ try {
202
+ return await this.session.request(value.backend, 'GET', apiPath, undefined, true);
203
+ }
204
+ catch {
205
+ this.options.signal?.throwIfAborted();
206
+ return undefined;
207
+ }
208
+ };
209
+ // The release detail carries no name or repository; the directory projection does.
210
+ const page = await read('/api/v1/resource-releases/?search=' + encodeURIComponent(value.resource_release_uid) + '&limit=100');
211
+ const row = Array.isArray(page?.results) ? page.results.find((item) => typeof item?.uid === 'string' && item.uid.toLowerCase() === value.resource_release_uid) : undefined;
212
+ if (row)
213
+ Object.assign(found, labels({ name: row.name, code_repository_branch_uid: row.code_repository_branch_uid }));
214
+ const branchUid = found.code_repository_branch_uid ?? value.code_repository_branch_uid;
215
+ if (branchUid) {
216
+ const branch = await read('/api/v1/code-repository-branches/' + encodeURIComponent(branchUid) + '/');
217
+ try {
218
+ const parsed = (0, repositoryModels_1.codeRepositoryBranchFromResponse)(branch, 'CodeRepositoryBranch response');
219
+ if (parsed.uid.toLowerCase() === branchUid)
220
+ Object.assign(found, labels({ display_name: parsed.code_repository_name, code_repository_branch_uid: branchUid }));
221
+ }
222
+ catch { /* an unreadable branch leaves the repository name out */ }
223
+ }
224
+ return found;
225
+ }
226
+ labelled(value, found) {
227
+ return connection(value.backend, { ...value, ...found });
228
+ }
229
+ /** Optional discovery labels, deduplicated by branch. Never writes connection metadata. */
230
+ async discoveryNames(rows) {
231
+ const pending = [...new Set(rows.filter(row => !row.display_name).flatMap(row => row.code_repository_branch_uid ? [row.code_repository_branch_uid] : []))];
232
+ const names = new Map();
233
+ await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
234
+ while (pending.length) {
235
+ this.options.signal?.throwIfAborted();
236
+ const uid = pending.shift();
237
+ try {
238
+ const raw = await this.session.request(rows[0].backend, 'GET', '/api/v1/code-repository-branches/' + uid + '/');
239
+ const branch = (0, repositoryModels_1.codeRepositoryBranchFromResponse)(raw, 'CodeRepositoryBranch response');
240
+ if (branch.uid.toLowerCase() === uid)
241
+ names.set(uid, branch.code_repository_name);
242
+ }
243
+ catch (error) {
244
+ this.options.signal?.throwIfAborted();
245
+ if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
246
+ throw error;
247
+ await this.options.checkAccount?.();
248
+ // Unavailable optional names must not hide usable endpoints.
249
+ }
250
+ }
251
+ }));
252
+ return rows.map(row => !row.display_name && row.code_repository_branch_uid && names.has(row.code_repository_branch_uid)
253
+ ? this.labelled(row, { display_name: names.get(row.code_repository_branch_uid) }) : row);
254
+ }
255
+ /** Records saved before labels were stored are completed once, when the backend answers. */
256
+ async backfill(rows) {
257
+ const pending = rows.filter(row => !row.display_name);
258
+ if (!pending.length)
259
+ return rows;
260
+ const found = new Map();
261
+ await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
262
+ while (pending.length) {
263
+ const row = pending.shift();
264
+ const resolved = await this.resolveLabels(row);
265
+ const added = Object.entries(resolved).filter(([key, item]) => item !== row[key]);
266
+ if (added.length)
267
+ found.set(row.backend + ' ' + row.resource_release_uid, Object.fromEntries(added));
268
+ }
269
+ }));
270
+ if (!found.size)
271
+ return rows;
272
+ const key = (row) => row.backend + ' ' + row.resource_release_uid;
273
+ const merge = (row) => found.has(key(row)) ? this.labelled(row, { ...found.get(key(row)) }) : row;
274
+ try {
275
+ await this.update(all => all.map(merge));
276
+ }
277
+ catch (error) {
278
+ // A held lock or a failed write keeps the listing itself available.
279
+ if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
280
+ throw error;
281
+ this.options.signal?.throwIfAborted();
282
+ }
283
+ return rows.map(merge);
284
+ }
285
+ async dispatch(request, inputBackend) {
286
+ let callStarted = false, resultData;
287
+ try {
288
+ const backend = (0, session_1.requireBackend)(inputBackend);
289
+ this.options.signal?.throwIfAborted();
290
+ if (request.action === 'mcp-list' && request.readOnly !== undefined && typeof request.readOnly !== 'boolean'
291
+ || request.action === 'mcp-discover' && request.enrichNames !== undefined && typeof request.enrichNames !== 'boolean') {
292
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Discovery and listing options must be booleans.');
293
+ }
294
+ const environmentUid = request.environmentUid === undefined ? undefined : uuid(request.environmentUid);
295
+ const belongs = (value) => !environmentUid || value.mcp_connection.organization_environment_uid === environmentUid;
296
+ const requireEnvironment = (value) => {
297
+ if (!belongs(value))
298
+ throw new models_1.ToolingError('INVALID_REQUEST', 'This MCP belongs to a different organization environment. Select that environment first.');
299
+ };
300
+ let data, summary;
301
+ if (request.action === 'mcp-environments') {
302
+ data = await (0, environments_1.listEnvironments)(this.session, backend, this.options.signal);
303
+ summary = 'Organization environments available to your MainSequence account.';
304
+ }
305
+ else if (request.action === 'mcp-discover') {
306
+ if ((request.search !== undefined && typeof request.search !== 'string')
307
+ || (request.limit !== undefined && (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 100))
308
+ || (request.offset !== undefined && (!Number.isInteger(request.offset) || request.offset < 0))) {
309
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Discovery requires a text search, a limit from 1 to 100, and a nonnegative offset.');
310
+ }
311
+ const client = new mcpTransport_1.McpTransport(message => this.session.request(backend, 'POST', '/mcp', message, true));
312
+ await client.initialize();
313
+ const query = async (offset, limit) => {
314
+ const output = await client.request('tools/call', { name: 'resource_release.mcp_endpoints',
315
+ arguments: { ...(request.search !== undefined ? { search: request.search } : {}), limit, offset } });
316
+ if (output?.isError)
317
+ throw new models_1.ToolingError('API_ERROR', 'Platform MCP could not list available project endpoints.');
318
+ const page = output?.structuredContent;
319
+ if (!Number.isInteger(page?.count) || page.count < 0 || !Array.isArray(page?.results))
320
+ throw new models_1.ToolingError('API_ERROR', 'Platform MCP returned an invalid directory.');
321
+ return { count: page.count, results: page.results.map((row) => connection(backend, row)) };
322
+ };
323
+ if (!environmentUid)
324
+ data = await query(request.offset ?? 0, request.limit ?? 100);
325
+ else {
326
+ // The existing platform MCP has no environment filter. Scan its pages
327
+ // before paginating the matching releases; never show an incomplete page as complete.
328
+ const matching = new Map();
329
+ for (let offset = 0;; offset += 100) {
330
+ this.options.signal?.throwIfAborted();
331
+ if (offset >= 10000)
332
+ throw new models_1.ToolingError('API_ERROR', 'MCP discovery is too large; narrow the search.');
333
+ const page = await query(offset, 100);
334
+ for (const value of page.results.filter(belongs))
335
+ matching.set(value.resource_release_uid, value);
336
+ if (offset + 100 >= page.count)
337
+ break;
338
+ }
339
+ const values = [...matching.values()], offset = request.offset ?? 0;
340
+ data = { count: values.length, results: values.slice(offset, offset + (request.limit ?? 100)) };
341
+ }
342
+ if (request.enrichNames) {
343
+ const page = data;
344
+ data = { ...page, results: await this.discoveryNames(page.results) };
345
+ }
346
+ summary = 'Available project MCP endpoints; discovery does not save a connection.';
347
+ }
348
+ else if (request.action === 'mcp-list') {
349
+ const saved = (await this.read()).filter(row => row.backend === backend && belongs(row));
350
+ data = request.readOnly ? saved : await this.backfill(saved);
351
+ summary = 'Saved MCP connections for this backend. Availability is checked when used.';
352
+ }
353
+ else {
354
+ const releaseUid = uuid(request.releaseUid);
355
+ if (request.action === 'mcp-remove') {
356
+ const saved = (await this.read()).find(row => row.backend === backend && row.resource_release_uid === releaseUid);
357
+ if (saved)
358
+ requireEnvironment(saved);
359
+ await this.update(rows => rows.filter(row => !(row.backend === backend && row.resource_release_uid === releaseUid)));
360
+ data = { resource_release_uid: releaseUid };
361
+ summary = 'The saved connection was removed.';
362
+ }
363
+ else {
364
+ let value = await this.current(backend, releaseUid);
365
+ requireEnvironment(value);
366
+ if (request.action !== 'mcp-add') {
367
+ const saved = (await this.read()).find(row => row.backend === backend && row.resource_release_uid === releaseUid);
368
+ if (!saved)
369
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Add this MCP connection before using it.');
370
+ if (saved.mcp_connection.url !== value.mcp_connection.url)
371
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The MCP endpoint changed. Add the release again to confirm its new endpoint.');
372
+ value = this.labelled(value, labels(saved));
373
+ }
374
+ const client = await this.open(value);
375
+ const tools = await client.tools();
376
+ if (request.action === 'mcp-add') {
377
+ value = this.labelled(value, await this.resolveLabels(value));
378
+ await this.update(rows => [...rows.filter(row => !(row.backend === backend && row.resource_release_uid === releaseUid)), value]);
379
+ data = { connection: value, tools };
380
+ summary = 'MCP connection verified and saved. Its tools can now be used through the plugin.';
381
+ }
382
+ else if (request.action === 'mcp-tools') {
383
+ data = { connection: value, tools };
384
+ summary = 'Current tools from the selected MCP.';
385
+ }
386
+ else if (request.action === 'mcp-call') {
387
+ if (typeof request.tool !== 'string' || !tools.some(tool => tool.name === request.tool))
388
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The selected tool is absent from the current MCP catalog.');
389
+ if (!request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments))
390
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Tool arguments must be a JSON object.');
391
+ this.options.signal?.throwIfAborted();
392
+ callStarted = true;
393
+ const output = await client.request('tools/call', { name: request.tool, arguments: request.arguments });
394
+ if (!output || !Array.isArray(output.content))
395
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool returned an invalid result.');
396
+ resultData = output;
397
+ if (output.isError)
398
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool reported an error. Inspect its result before retrying.');
399
+ data = output;
400
+ summary = 'The MCP tool completed.';
401
+ }
402
+ else
403
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Unknown MCP action.');
404
+ }
405
+ }
406
+ await this.options.checkAccount?.();
407
+ return this.sanitize({ schemaVersion: 1, action: request.action, status: 'success', summary, data });
408
+ }
409
+ catch (error) {
410
+ const failure = this.options.signal?.aborted ? new models_1.ToolingError('CANCELLED', 'The MCP action was cancelled.')
411
+ : error instanceof models_1.ToolingError ? error : new models_1.ToolingError('INTERNAL_ERROR', 'The MCP action could not complete.');
412
+ return this.sanitize({ schemaVersion: 1, action: request.action, status: failure.code === 'CANCELLED' ? 'cancelled' : 'failure',
413
+ summary: failure.message, error: { code: failure.code, message: failure.message }, ...(resultData === undefined ? {} : { data: resultData }),
414
+ ...(callStarted ? { effects: { state: 'partial-or-uncertain', message: 'The tool may have changed remote state. No automatic retry was attempted; verify the outcome before repeating it.' } } : {}) });
415
+ }
416
+ finally {
417
+ this.secrets.clear();
418
+ }
419
+ }
420
+ }
421
+ exports.McpConnections = McpConnections;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpTransport = void 0;
4
+ exports.readMcpResponse = readMcpResponse;
5
+ exports.sendProjectMcp = sendProjectMcp;
6
+ const models_1 = require("./models.js");
7
+ /** The platform's stateless Streamable HTTP profile; no legacy SSE endpoint. */
8
+ class McpTransport {
9
+ send;
10
+ sequence = 0;
11
+ initialized = false;
12
+ constructor(send) {
13
+ this.send = send;
14
+ }
15
+ async initialize() {
16
+ const result = await this.request('initialize', {
17
+ protocolVersion: '2025-11-25', capabilities: {},
18
+ clientInfo: { name: 'mainsequence-devtools', version: '0.2.7' },
19
+ });
20
+ if (result?.protocolVersion !== '2025-11-25' || !result?.capabilities?.tools) {
21
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'This endpoint does not support the platform MCP tools profile (2025-11-25).');
22
+ }
23
+ await this.send({ jsonrpc: '2.0', method: 'notifications/initialized' });
24
+ this.initialized = true;
25
+ }
26
+ async request(method, params) {
27
+ if (method !== 'initialize' && !this.initialized)
28
+ throw new models_1.ToolingError('API_ERROR', 'The MCP connection has not initialized.');
29
+ const id = ++this.sequence;
30
+ const response = await this.send({ jsonrpc: '2.0', id, method, params });
31
+ if (!response || response.jsonrpc !== '2.0' || response.id !== id) {
32
+ throw new models_1.ToolingError('API_ERROR', 'The MCP endpoint returned an invalid or mismatched response.');
33
+ }
34
+ if (response.error)
35
+ throw new models_1.ToolingError('API_ERROR', 'The MCP endpoint rejected ' + method
36
+ + (Number.isInteger(response.error.code) ? ' (JSON-RPC ' + response.error.code + ')' : '') + '.');
37
+ if (!Object.hasOwn(response, 'result'))
38
+ throw new models_1.ToolingError('API_ERROR', 'The MCP response has no result.');
39
+ return response.result;
40
+ }
41
+ async tools() {
42
+ const tools = [], cursors = new Set(), names = new Set();
43
+ let cursor;
44
+ do {
45
+ const page = await this.request('tools/list', cursor ? { cursor } : {});
46
+ if (!Array.isArray(page?.tools))
47
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool catalog is invalid.');
48
+ for (const tool of page.tools) {
49
+ if (!tool || typeof tool.name !== 'string' || !tool.name || !tool.inputSchema || typeof tool.inputSchema !== 'object' || Array.isArray(tool.inputSchema) || names.has(tool.name)) {
50
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool catalog contains an invalid or duplicate tool.');
51
+ }
52
+ const normalized = { ...tool };
53
+ if (typeof normalized.description !== 'string')
54
+ delete normalized.description;
55
+ for (const field of ['annotations', '_meta'])
56
+ if (!normalized[field] || typeof normalized[field] !== 'object' || Array.isArray(normalized[field]))
57
+ delete normalized[field];
58
+ if (normalized.annotations) {
59
+ normalized.annotations = { ...normalized.annotations };
60
+ for (const flag of ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint']) {
61
+ if (typeof normalized.annotations[flag] !== 'boolean')
62
+ delete normalized.annotations[flag];
63
+ }
64
+ }
65
+ names.add(tool.name);
66
+ tools.push(normalized);
67
+ }
68
+ cursor = page.nextCursor;
69
+ if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || cursors.has(cursor))) {
70
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool catalog returned an invalid pagination cursor.');
71
+ }
72
+ if (cursor)
73
+ cursors.add(cursor);
74
+ if (tools.length > 10_000 || cursors.size > 100)
75
+ throw new models_1.ToolingError('API_ERROR', 'The MCP tool catalog exceeded the client limit.');
76
+ } while (cursor);
77
+ return tools;
78
+ }
79
+ }
80
+ exports.McpTransport = McpTransport;
81
+ /** Read bounded JSON or POST SSE; never reconnect or replay a tool invocation. */
82
+ async function readMcpResponse(response, message) {
83
+ if (response.headers.has('mcp-session-id')) {
84
+ await response.body?.cancel();
85
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'Stateful MCP sessions are outside the platform profile.');
86
+ }
87
+ if (!response.ok) {
88
+ await response.body?.cancel();
89
+ throw new models_1.ToolingError(response.status === 401 || response.status === 403 ? 'AUTH_REJECTED' : 'API_ERROR', 'The MCP endpoint returned HTTP ' + response.status + '.');
90
+ }
91
+ if (!Object.hasOwn(message, 'id')) {
92
+ await response.body?.cancel();
93
+ if (response.status !== 202 && response.status !== 204)
94
+ throw new models_1.ToolingError('API_ERROR', 'The MCP notification was not acknowledged.');
95
+ return undefined;
96
+ }
97
+ const kind = response.headers.get('content-type')?.split(';')[0].trim();
98
+ if (kind !== 'application/json' && kind !== 'text/event-stream') {
99
+ await response.body?.cancel();
100
+ throw new models_1.ToolingError('API_ERROR', 'The MCP endpoint returned an unsupported content type.');
101
+ }
102
+ if (!response.body)
103
+ throw new models_1.ToolingError('API_ERROR', 'The MCP endpoint returned an empty response.');
104
+ const reader = response.body.getReader(), decoder = new TextDecoder();
105
+ let size = 0, text = '';
106
+ const parse = (raw) => {
107
+ try {
108
+ return JSON.parse(raw);
109
+ }
110
+ catch {
111
+ throw new models_1.ToolingError('API_ERROR', 'The MCP endpoint returned invalid JSON.');
112
+ }
113
+ };
114
+ try {
115
+ while (true) {
116
+ const part = await reader.read();
117
+ if (part.done)
118
+ break;
119
+ size += part.value.byteLength;
120
+ if (size > 8_000_000)
121
+ throw new models_1.ToolingError('API_ERROR', 'The MCP response exceeded the client limit.');
122
+ text += decoder.decode(part.value, { stream: true });
123
+ if (kind === 'text/event-stream') {
124
+ // Normalize CRLF only once both bytes have arrived; chunk boundaries may split them.
125
+ text = text.replace(/\r\n/g, '\n');
126
+ let end;
127
+ while ((end = text.indexOf('\n\n')) >= 0) {
128
+ const event = text.slice(0, end);
129
+ text = text.slice(end + 2);
130
+ const data = event.split('\n').filter(line => line.startsWith('data:')).map(line => line.slice(5).replace(/^ /, '')).join('\n');
131
+ if (!data)
132
+ continue;
133
+ const value = parse(data);
134
+ if (value?.id === message.id && !value.method)
135
+ return value;
136
+ if (value?.method && Object.hasOwn(value, 'id'))
137
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'The MCP endpoint requested an unsupported client capability.');
138
+ if (value && Object.hasOwn(value, 'id'))
139
+ throw new models_1.ToolingError('API_ERROR', 'The MCP stream returned a mismatched response.');
140
+ }
141
+ }
142
+ }
143
+ text += decoder.decode();
144
+ if (kind === 'application/json')
145
+ return parse(text);
146
+ throw new models_1.ToolingError('API_ERROR', 'The MCP stream ended before the response arrived. The call was not retried.');
147
+ }
148
+ finally {
149
+ await reader.cancel().catch(() => { });
150
+ reader.releaseLock();
151
+ }
152
+ }
153
+ async function sendProjectMcp(url, token, message, options) {
154
+ options.signal?.throwIfAborted();
155
+ const controller = new AbortController(), abort = () => controller.abort();
156
+ options.signal?.addEventListener('abort', abort, { once: true });
157
+ const timer = setTimeout(abort, options.requestTimeoutMs ?? 60_000);
158
+ try {
159
+ return await readMcpResponse(await (options.fetch ?? fetch)(url, {
160
+ method: 'POST', redirect: 'error', signal: controller.signal,
161
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream',
162
+ Authorization: 'Bearer ' + token, 'MCP-Protocol-Version': '2025-11-25' },
163
+ body: JSON.stringify(message),
164
+ }), message);
165
+ }
166
+ catch (error) {
167
+ if (options.signal?.aborted)
168
+ throw new models_1.ToolingError('CANCELLED', 'The MCP request was cancelled. Cancellation does not undo a remote operation.');
169
+ if (error instanceof models_1.ToolingError)
170
+ throw error;
171
+ throw new models_1.ToolingError('BACKEND_UNREACHABLE', 'The MCP request failed or timed out. It was not retried.');
172
+ }
173
+ finally {
174
+ clearTimeout(timer);
175
+ options.signal?.removeEventListener('abort', abort);
176
+ }
177
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ToolingError = void 0;
4
+ class ToolingError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'ToolingError';
10
+ }
11
+ }
12
+ exports.ToolingError = ToolingError;