Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/conversational-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ If no developer session exists, use the existing Main Sequence hosted login. A c

Connection metadata lives in mcp-connections.json beside the existing MainSequence CLI configuration, separated by backend and release UID. Reads/calls recheck release access and fetch the current tool catalog. A changed endpoint requires an explicit add again. Removing a connection does not revoke your login.

The release UID identifies a connection. Its optional `name` is a display label from discovery; release detail responses omit it. Adding a connection and reading or calling its tools work without that label, including for saved connections. UID-based operations do not repeat discovery to obtain a name.

Project tools enforce their ordinary permissions. The client adds no agent-specific allowlist or exclusions.

## Failures and limits
Expand Down
2 changes: 1 addition & 1 deletion manifests/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mainsequence-devtools",
"version": "0.2.6",
"version": "0.2.7",
"description": "Use MainSequence project MCPs from conversation and run explicit developer commands with the bundled runner.",
"author": {
"name": "MainSequence"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dev-mainsequence/devtools",
"version": "0.2.6",
"version": "0.2.7",
"description": "Shared MainSequence developer actions, project operations and deterministic Codex adapter",
"license": "MIT",
"engines": {
Expand Down
7 changes: 4 additions & 3 deletions src/shared/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type McpRequest = { backend?: string; environmentUid?: string } & (
| { action: 'mcp-call'; releaseUid: string; tool: string; arguments: Record<string, unknown> }
);
type Connection = {
backend: string; resource_release_uid: string; name: string;
backend: string; resource_release_uid: string; name?: string;
mcp_connection: { url: string; transport: 'streamable-http'; resource_metadata_url: string;
organization_environment_uid: string; organization_environment_name?: string; active_revision_uid: string };
};
Expand All @@ -37,7 +37,7 @@ function endpoint(value: unknown, metadata = false): string {
}
function connection(backend: string, row: any): Connection {
const c = row?.mcp_connection;
if (!c || c.transport !== 'streamable-http' || typeof row.name !== 'string' || !row.name.trim()) {
if (!c || c.transport !== 'streamable-http') {
throw new ToolingError('API_ERROR', 'The selected release has no active platform MCP connection.');
}
const url = endpoint(c.url);
Expand All @@ -46,7 +46,8 @@ function connection(backend: string, row: any): Connection {
const metadataQuery = new URL(metadataUrl).searchParams;
if ([...metadataQuery.keys()].some(key => key !== 'resource_release_uid') || metadataQuery.getAll('resource_release_uid').length !== 1
|| metadataQuery.get('resource_release_uid') !== releaseUid) throw new ToolingError('API_ERROR', 'The MCP metadata targets another release.');
return { backend: requireBackend(backend), resource_release_uid: releaseUid, name: row.name,
return { backend: requireBackend(backend), resource_release_uid: releaseUid,
...(typeof row.name === 'string' && row.name.trim() ? { name: row.name } : {}),
mcp_connection: { url, transport: 'streamable-http', resource_metadata_url: metadataUrl,
organization_environment_uid: uuid(c.organization_environment_uid),
...(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) } };
Expand Down
2 changes: 1 addition & 1 deletion src/shared/mcpTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class McpTransport {
async initialize(): Promise<void> {
const result = await this.request('initialize', {
protocolVersion: '2025-11-25', capabilities: {},
clientInfo: { name: 'mainsequence-devtools', version: '0.2.2' },
clientInfo: { name: 'mainsequence-devtools', version: '0.2.7' },
});
if (result?.protocolVersion !== '2025-11-25' || !result?.capabilities?.tools) {
throw new ToolingError('UNSUPPORTED_ACTION', 'This endpoint does not support the platform MCP tools profile (2025-11-25).');
Expand Down
63 changes: 63 additions & 0 deletions tests/mcp.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ async function fixture(t) {
if (state.denied) return res.writeHead(403).end();
if (req.url === '/api/v1/resource-releases/' + uid + '/' || req.url === '/api/v1/resource-releases/' + secondUid + '/') {
const value = row(req.url.includes(secondUid) ? secondUid : uid);
// Release detail has capability metadata, but no discovery-only display name.
delete value.name;
value.mcp_enabled = !state.noMcp;
if (state.noMcp) value.mcp_connection = null;
Object.assign(value, state.detail);
res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(value));
}
if (req.url.endsWith('/exchange-launch/')) {
Expand Down Expand Up @@ -124,6 +128,65 @@ test('conversation workflow discovers, saves, immediately calls and remembers MC
assert.equal((await f.dispatch('mcp-call', { tool: 'set_value', arguments: {} })).status, 'failure');
});

test('release detail and saved connections work without a display name or another discovery request', async t => {
const f = await fixture(t);
const detail = await fetch(f.backend + '/api/v1/resource-releases/' + f.uid + '/', {
headers: { Authorization: 'Bearer private-platform-token' },
}).then(response => response.json());
assert.equal(Object.hasOwn(detail, 'name'), false, 'detail matches the backend contract');
assert.equal(detail.mcp_enabled, true);
const added = await f.dispatch('mcp-add');
assert.equal(added.status, 'success');
assert.equal(added.data.connection.resource_release_uid, f.uid);
assert.equal(Object.hasOwn(added.data.connection, 'name'), false);
assert.equal(Object.hasOwn(JSON.parse(await fs.readFile(f.file, 'utf8'))[0], 'name'), false);

// Existing named records and records with no usable label remain readable.
for (const name of [undefined, 'legacy display label', '', null, 17]) {
const saved = { ...added.data.connection, name };
await fs.writeFile(f.file, JSON.stringify([saved]));
const listed = await f.dispatch('mcp-list', {}, f.create());
assert.equal(listed.status, 'success');
assert.equal(listed.data[0].name, name === 'legacy display label' ? name : undefined);
assert.equal((await f.dispatch('mcp-tools', {}, f.create())).status, 'success');
const called = await f.dispatch('mcp-call', { tool: 'set_value', arguments: { value: 12 } }, f.create());
assert.equal(called.status, 'success');
assert.equal(called.data.structuredContent.value, 12);
}
assert.equal(f.calls.some(call => call.server === 'platform' && call.path === '/mcp'), false,
'UID-based operations do not fetch discovery to fill display metadata');
});

test('nameless release details still validate identity and all required MCP metadata before exchanging credentials', async t => {
const f = await fixture(t);
assert.equal((await f.dispatch('mcp-add')).status, 'success');
const connection = f.row().mcp_connection;
const invalidDetails = [
{ uid: f.secondUid, mcp_connection: f.row(f.secondUid).mcp_connection },
{ mcp_connection: null },
...[
{ transport: 'sse' },
{ url: f.project + '/other' },
{ resource_metadata_url: f.row(f.secondUid).mcp_connection.resource_metadata_url },
{ organization_environment_uid: 'invalid' },
{ active_revision_uid: 'invalid' },
].map(fields => ({ mcp_connection: { ...connection, ...fields } })),
];
const saved = await fs.readFile(f.file, 'utf8');
const exchanged = f.state.exchanged, projectCalls = f.calls.filter(call => call.server === 'project').length;
for (const detail of invalidDetails) {
f.state.detail = detail;
for (const action of ['mcp-add', 'mcp-tools', 'mcp-call']) {
const result = await f.dispatch(action, { tool: 'set_value', arguments: { value: 1 } });
assert.equal(result.status, 'failure', JSON.stringify({ action, detail }));
}
}
assert.equal(f.state.exchanged, exchanged);
assert.equal(f.calls.filter(call => call.server === 'project').length, projectCalls);
assert.equal(f.state.invoked, 0);
assert.equal(await fs.readFile(f.file, 'utf8'), saved);
});

test('revocation, removed capabilities, changed URLs, and a wrong credential target stop before project execution', async t => {
const f = await fixture(t); await f.dispatch('mcp-add');
f.state.denied = true;
Expand Down
Loading