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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,12 @@ relabels the remaining MCPs **Unnamed MCP** or drops their tools: the pane keeps
resolved name and the previously registered tools, names the unread MCP in a warning, and
the worker falls back to the release's own name. See the
[release notes](docs/releases/0.4.17.md).

### 0.4.18 — MCP names stored with the connection

A connected MCP's names live with its saved connection: devtools 0.3.1 reads the
release's name and its repository's name when the MCP is connected and stores them, so a
listing labels the connection without asking the backend. A connection saved by an earlier
version is completed by the first listing that can read its names. Only the Add MCP search
still labels its rows live, from the repository branch each row names. See the
[release notes](docs/releases/0.4.18.md).
34 changes: 34 additions & 0 deletions docs/claude-desktop-verification.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# Claude Desktop plugin verification

## 0.4.18 — MCP names stored with the saved connection (2026-10-21)

Cause of the recurring **Unnamed MCP**: a saved connection held only its release UID and
endpoint, because the release detail that `mcp-add` reads carries neither a name nor a
repository. Every listing therefore asked the backend twice (the release directory, then
the repository branch) to label the row, and any failure of those requests showed the
UID. 0.4.17 only kept the last resolved name. Fix (devtools 0.3.1, PR
mainsequence-sdk/mainsequence-devtools#15): `mcp-add` resolves the release's name and its
repository's name once, when the MCP is connected, and stores them with the connection as
`name`, `display_name` and `code_repository_branch_uid`; `mcp-list` returns them without
a backend request and completes a record saved by an earlier version the first time the
backend can be read, under the connection lock. The worker's name adapter
(`src/hosts/claudeMcpNames.ts`) now labels only discovery rows, from the
`code_repository_branch_uid` each row carries; it no longer touches listings.

Live check on 2026-10-21 against the local backend `http://127.0.0.1:8000` (environment
`18595d12-…`, Production) with the rebuilt worker and the saved MainSequenceCRMAPI
connection as 0.4.17 had left it, with no name in the file:

| Worker action | Result | Duration |
| --- | --- | --- |
| `mcp-list`, first run | `crm` · `MainSequenceCRMAPI`, written to `mcp-connections.json` | 5.6 s |
| `mcp-list`, second run | the same labels from the file, no backend request | 46 ms |
| `mcp-tools` | the connection labelled, 70 tools | 16.7 s |
| `mcp-discover` "crm" | the row labelled `MainSequenceCRMAPI` from its branch | 7.8 s |

Automated verification (`npm run release:check`): 97 Node tests (the packaged-worker MCP
test now covers stored names, listing without a backend request, the one-time completion
of an old record, and denied directory or branch reads), ten native simulator tests and
loader validation on Desktop's bundled Code 2.1.304, documentation, packaging and
distribution checks. devtools 0.3.1 adds its own test for storage, local listing,
completion, denied and mismatched lookups, malformed stored labels, a held lock and
discovery rows (112 tests, CI on Ubuntu and macOS).

## 0.4.17 — MCP refresh tolerates a briefly unreachable backend (2026-10-21)

Reported: after disconnecting one MCP, the remaining CRM MCP showed as **Unnamed MCP**
Expand Down
11 changes: 8 additions & 3 deletions docs/claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,14 @@ are available from the next prompt. Exact release identities remain pinned.

Connecting or disconnecting an MCP re-reads the environment's saved connections and
each one's tool catalog (since 0.4.17, a refresh tolerates a backend that is briefly
unreachable, such as a local development server restarting). An MCP's row keeps the
last name the backend resolved for it while the name lookup is unavailable, and the
release's own name labels a connection whose repository cannot be read. A connection
unreachable, such as a local development server restarting). Since 0.4.18 a connection's
names are stored with it: connecting an MCP reads the release's name and its repository's
name and saves them in `mcp-connections.json` as `name`, `display_name` and
`code_repository_branch_uid`, so a listing labels the connection without a backend
request, and a connection saved by an earlier version is completed by the first listing
that can read its names. A name that cannot be read is left out and never makes a working
MCP unavailable; the row then keeps the last name the pane resolved for it, or the
release's own name when only its repository could not be read. A connection
whose catalog cannot be read right now keeps the tools registered by the previous
refresh and is named in a warning under the list; **Refresh** or **Re-register tools**
reads it again. Only a disconnected MCP loses its tools, and a failed listing keeps the
Expand Down
21 changes: 21 additions & 0 deletions docs/releases/0.4.18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
A connected MCP's names now live with its saved connection. Until 0.4.17 the saved record
held only the release UID and endpoint, because the release detail read when connecting
carries neither a name nor a repository; every listing then asked the backend twice (the
release directory, then the repository branch) to label the row, and a backend that was
restarting during a refresh showed **Unnamed MCP**. devtools 0.3.1 reads the release's
name and its repository's name once, when the MCP is connected, and stores them in
`mcp-connections.json` as `name`, `display_name` and `code_repository_branch_uid`. A
listing shows them without a backend request, and a connection saved by an earlier
version is completed by the first listing that can read its names. A name that cannot be
read is left out and never makes a working MCP unavailable. The Add MCP search still
labels its rows live, from the repository branch each row names.

Validation: 97 Node tests (the packaged-worker MCP test covers stored names, listing
without a backend request, the one-time completion of an old record, and denied directory
or branch reads), ten native Desktop simulator tests, loader validation and distribution
checks on Desktop's bundled Code 2.1.304. Against the local backend, the saved
MainSequenceCRMAPI connection that 0.4.17 had left without a name was completed by the
first listing (5.6 s) and listed from the file afterwards (46 ms, no backend request);
its tool catalog and the Add MCP search carried the same labels. Bundles devtools 0.3.1.
Update from the MainSequence marketplace and reload the plugin in the Code chat. Node 22+
required.
2 changes: 1 addition & 1 deletion manifests/claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mainsequence",
"version": "0.4.17",
"version": "0.4.18",
"description": "MainSequence login, repositories, projects, MCP, agents and activity inside Claude Desktop Code.",
"author": {
"name": "MainSequence"
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dev-mainsequence/claude-desktop",
"version": "0.4.17",
"version": "0.4.18",
"private": true,
"description": "MainSequence integration for Claude Desktop Code",
"license": "MIT",
Expand All @@ -24,7 +24,7 @@
"verify:published": "node scripts/verify-public-release.cjs"
},
"dependencies": {
"@dev-mainsequence/devtools": "0.2.8"
"@dev-mainsequence/devtools": "0.3.1"
},
"devDependencies": {
"@types/node": "18.19.130",
Expand Down
74 changes: 24 additions & 50 deletions src/hosts/claudeMcpNames.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,32 @@
import { ActionResult, HostServices, RepositoryClient, createSessionService } from '@dev-mainsequence/devtools';

/** Presentation metadata only. Discovery, saved connections and tool routing stay in devtools. */
/** Presentation metadata for discovery rows only. A saved connection carries its names
* (devtools stores them when an MCP is connected), so listings need no lookup here. */
export async function withClaudeMcpNames(result: ActionResult, options: { host: HostServices; signal?: AbortSignal }): Promise<ActionResult> {
if (result.status !== 'success' || !['mcp-discover', 'mcp-list'].includes(result.action)) return result;
if (result.status !== 'success' || result.action !== 'mcp-discover') return result;
const data = result.data as any;
const connections: any[] = Array.isArray(data) ? data : data?.results;
if (!Array.isArray(connections) || !connections.length) return result;
const backend = connections[0].backend;
const rows: any[] = Array.isArray(data?.results) ? data.results : [];
const pending = [...new Set(rows.map(row => row.code_repository_branch_uid).filter(uid => typeof uid === 'string' && uid.trim()))] as string[];
if (!pending.length) return result;
const session = createSessionService(options.host, { signal: options.signal, requestTimeoutMs: 15_000 });
const repositories = new RepositoryClient(session, backend);
const wanted = new Map(connections.map(row => [row.resource_release_uid, row]));
const branches = new Map<string, string>();
const releaseNames = new Map<string, string>();
try {
// The pinned devtools result omits repository identity. Read the existing
// collection projection, matching exact releases and environments, to label it.
for (let offset = 0; offset < 10000; offset += 100) {
const page = await session.request(backend, 'GET', `/api/v1/resource-releases/?mcp_available=true&limit=100&offset=${offset}`) as any;
if (!Array.isArray(page?.results) || !Number.isInteger(page.count) || page.count < 0) break;
for (const row of page.results) {
const connection = wanted.get(row.uid);
if (connection?.backend === backend
&& connection.mcp_connection?.organization_environment_uid === row.mcp_connection?.organization_environment_uid
&& typeof row.code_repository_branch_uid === 'string' && row.code_repository_branch_uid.trim()) {
branches.set(row.uid, row.code_repository_branch_uid);
// The release's own name labels it when its repository cannot be read.
if (typeof row.name === 'string' && row.name.trim()) releaseNames.set(row.uid, row.name.trim());
}
const repositories = new RepositoryClient(session, rows[0].backend);
const names = new Map<string, string>();
// Keep a large directory page from opening an unbounded number of requests.
await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
while (pending.length) {
const uid = pending.shift()!;
try {
const branch = await repositories.getCodeRepositoryBranch(uid);
if (branch.uid === uid) names.set(uid, branch.code_repository_name);
} catch {
options.signal?.throwIfAborted();
// Optional display metadata must not make a working MCP unavailable.
}
if (branches.size === wanted.size || offset + 100 >= page.count || !page.results.length) break;
}
const names = new Map<string, string>();
const pending = [...new Set(branches.values())];
// Keep large saved catalogs from opening an unbounded number of requests.
await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
while (pending.length) {
const uid = pending.shift()!;
try {
const branch = await repositories.getCodeRepositoryBranch(uid);
if (branch.uid === uid) names.set(uid, branch.code_repository_name);
} catch {
options.signal?.throwIfAborted();
// Optional display metadata must not make a working MCP unavailable.
}
}
}));
const labelled = connections.map(row => {
const name = names.get(branches.get(row.resource_release_uid) || '');
const release = typeof row.name === 'string' && row.name.trim() ? undefined : releaseNames.get(row.resource_release_uid);
return { ...row, ...(release ? { name: release } : {}), ...(name ? { display_name: name } : {}) };
});
return { ...result, data: Array.isArray(data) ? labelled : { ...data, results: labelled } };
} catch {
options.signal?.throwIfAborted();
return result;
}
}));
const labelled = rows.map(row => {
const name = names.get(row.code_repository_branch_uid);
return name ? { ...row, display_name: name } : row;
});
return { ...result, data: { ...data, results: labelled } };
}
Loading
Loading