From 0cab4d165381a91e1e4e66210f703e510a66bf4e Mon Sep 17 00:00:00 2001 From: core-devops Date: Thu, 21 May 2026 15:28:11 -0700 Subject: [PATCH] feat: export workspace target resolver --- .gitea/workflows/publish.yml | 8 ++-- package.json | 9 +++- src/__tests__/targets.test.ts | 79 ++++++++++++++++++++++++++++++++ src/index.ts | 2 + src/targets.ts | 86 +++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/targets.test.ts create mode 100644 src/targets.ts diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 574b946..7abe708 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -8,9 +8,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - with: { node-version: '20', registry-url: 'https://registry.npmjs.org' } + with: + node-version: '20' + registry-url: 'https://git.moleculesai.app/api/packages/molecule-ai/npm/' - run: npm install - run: npm run build - run: npm test - - run: npm publish --access public - env: { NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' } + - run: npm publish + env: { NODE_AUTH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' } diff --git a/package.json b/package.json index 8ac5472..c49bc81 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "@molecule-ai/mcp-server", - "version": "1.0.0", + "version": "1.1.0", "description": "MCP server for Molecule AI Agent Team \u2014 manage workspaces, agents, and skills from any AI coding tool", "type": "module", + "exports": { + ".": "./dist/index.js", + "./targets": "./dist/targets.js" + }, + "types": "./dist/index.d.ts", "bin": { "molecule-mcp": "./dist/index.js" }, @@ -25,7 +30,7 @@ "typescript": "^5.5.0" }, "publishConfig": { - "access": "public" + "registry": "https://git.moleculesai.app/api/packages/molecule-ai/npm/" }, "repository": { "type": "git", diff --git a/src/__tests__/targets.test.ts b/src/__tests__/targets.test.ts new file mode 100644 index 0000000..0188611 --- /dev/null +++ b/src/__tests__/targets.test.ts @@ -0,0 +1,79 @@ +import { formatTargetSummary, parseWorkspaceTargets } from "../targets.js"; + +describe("parseWorkspaceTargets", () => { + it("keeps the legacy single-platform comma-separated env shape", () => { + expect( + parseWorkspaceTargets({ + MOLECULE_PLATFORM_URL: "https://hongming.moleculesai.app/", + MOLECULE_WORKSPACE_IDS: "ws-a, ws-b", + MOLECULE_WORKSPACE_TOKENS: "tok-a,tok-b", + }), + ).toEqual([ + { workspaceId: "ws-a", token: "tok-a", platformUrl: "https://hongming.moleculesai.app" }, + { workspaceId: "ws-b", token: "tok-b", platformUrl: "https://hongming.moleculesai.app" }, + ]); + }); + + it("supports one platform URL per workspace", () => { + expect( + parseWorkspaceTargets({ + MOLECULE_PLATFORM_URLS: "https://hongming.moleculesai.app,https://agents-team.moleculesai.app/", + MOLECULE_WORKSPACE_IDS: "ws-hongming,ws-agents", + MOLECULE_WORKSPACE_TOKENS: "tok-hongming,tok-agents", + }), + ).toEqual([ + { workspaceId: "ws-hongming", token: "tok-hongming", platformUrl: "https://hongming.moleculesai.app" }, + { workspaceId: "ws-agents", token: "tok-agents", platformUrl: "https://agents-team.moleculesai.app" }, + ]); + }); + + it("supports the platform registration JSON shape as the canonical SSOT", () => { + expect( + parseWorkspaceTargets({ + MOLECULE_WORKSPACES_JSON: JSON.stringify([ + { + id: "workspace-id-local-to-hongming-org", + token: "tok-hongming", + platform_url: "https://hongming.moleculesai.app", + }, + { + id: "different-workspace-id-local-to-agents-team-org", + token: "tok-agents", + platform_url: "https://agents-team.moleculesai.app/", + }, + ]), + }), + ).toEqual([ + { + workspaceId: "workspace-id-local-to-hongming-org", + token: "tok-hongming", + platformUrl: "https://hongming.moleculesai.app", + }, + { + workspaceId: "different-workspace-id-local-to-agents-team-org", + token: "tok-agents", + platformUrl: "https://agents-team.moleculesai.app", + }, + ]); + }); + + it("rejects platform URL count drift", () => { + expect(() => + parseWorkspaceTargets({ + MOLECULE_PLATFORM_URLS: "https://one.example", + MOLECULE_WORKSPACE_IDS: "ws-a,ws-b", + MOLECULE_WORKSPACE_TOKENS: "tok-a,tok-b", + }), + ).toThrow("MOLECULE_PLATFORM_URLS must have one URL per workspace"); + }); + + it("formats grouped target summaries without exposing tokens", () => { + expect( + formatTargetSummary([ + { workspaceId: "ws-a", token: "tok-a", platformUrl: "https://one.example" }, + { workspaceId: "ws-b", token: "tok-b", platformUrl: "https://one.example" }, + { workspaceId: "ws-c", token: "tok-c", platformUrl: "https://two.example" }, + ]), + ).toBe("https://one.example: ws-a, ws-b\n https://two.example: ws-c"); + }); +}); diff --git a/src/index.ts b/src/index.ts index 4f7380c..2c65c08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,8 @@ import { registerRemoteAgentTools } from "./tools/remote_agents.js"; // export triggers a compile error instead of a silent undefined at import. export { PLATFORM_URL, apiCall, isApiError, platformGet, toMcpResult, toMcpText } from "./api.js"; export type { ApiError } from "./api.js"; +export { formatTargetSummary, parseWorkspaceTargets } from "./targets.js"; +export type { WorkspaceTarget } from "./targets.js"; export { registerWorkspaceTools, diff --git a/src/targets.ts b/src/targets.ts new file mode 100644 index 0000000..554638d --- /dev/null +++ b/src/targets.ts @@ -0,0 +1,86 @@ +export interface WorkspaceTarget { + workspaceId: string; + token: string; + platformUrl: string; +} + +function splitList(raw: string | undefined): string[] { + return (raw ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} + +function trimUrl(raw: string): string { + return raw.trim().replace(/\/+$/, ""); +} + +export function parseWorkspaceTargets(env: Record): WorkspaceTarget[] { + const json = (env.MOLECULE_WORKSPACES_JSON ?? "").trim(); + if (json) { + let parsed: unknown; + try { + parsed = JSON.parse(json); + } catch (err) { + throw new Error(`MOLECULE_WORKSPACES_JSON is not valid JSON: ${err}`); + } + if (!Array.isArray(parsed)) { + throw new Error("MOLECULE_WORKSPACES_JSON must be an array"); + } + return parsed.map((entry, i) => { + if (!entry || typeof entry !== "object") { + throw new Error(`MOLECULE_WORKSPACES_JSON[${i}] must be an object`); + } + const row = entry as Record; + const workspaceId = String(row.id ?? row.workspace_id ?? "").trim(); + const token = String(row.token ?? row.workspace_token ?? "").trim(); + const platformUrl = trimUrl(String(row.platform_url ?? row.platformUrl ?? "")); + if (!workspaceId || !token || !platformUrl) { + throw new Error(`MOLECULE_WORKSPACES_JSON[${i}] requires id, token, and platform_url`); + } + return { workspaceId, token, platformUrl }; + }); + } + + const workspaceIds = splitList(env.MOLECULE_WORKSPACE_IDS); + const tokens = splitList(env.MOLECULE_WORKSPACE_TOKENS); + const platformUrls = splitList(env.MOLECULE_PLATFORM_URLS); + const singlePlatformUrl = trimUrl(env.MOLECULE_PLATFORM_URL ?? ""); + + if (workspaceIds.length === 0 || tokens.length === 0) { + return []; + } + if (workspaceIds.length !== tokens.length) { + throw new Error( + `MOLECULE_WORKSPACE_IDS and MOLECULE_WORKSPACE_TOKENS must have the same number of entries ` + + `(got ${workspaceIds.length} ids vs ${tokens.length} tokens)`, + ); + } + if (platformUrls.length > 0 && platformUrls.length !== workspaceIds.length) { + throw new Error( + `MOLECULE_PLATFORM_URLS must have one URL per workspace when set ` + + `(got ${platformUrls.length} urls vs ${workspaceIds.length} ids)`, + ); + } + if (platformUrls.length === 0 && !singlePlatformUrl) { + return []; + } + + return workspaceIds.map((workspaceId, i) => ({ + workspaceId, + token: tokens[i]!, + platformUrl: platformUrls.length > 0 ? trimUrl(platformUrls[i]!) : singlePlatformUrl, + })); +} + +export function formatTargetSummary(targets: WorkspaceTarget[]): string { + const byPlatform = new Map(); + for (const target of targets) { + const rows = byPlatform.get(target.platformUrl) ?? []; + rows.push(target.workspaceId); + byPlatform.set(target.platformUrl, rows); + } + return Array.from(byPlatform.entries()) + .map(([platformUrl, ids]) => `${platformUrl}: ${ids.join(", ")}`) + .join("\n "); +} -- 2.52.0