feat: export workspace target resolver #22

Merged
hongming merged 1 commits from ssot/workspace-targets-contract into main 2026-05-21 22:30:30 +00:00
5 changed files with 179 additions and 5 deletions
+5 -3
View File
@@ -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 }}' }
+7 -2
View File
@@ -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",
+79
View File
@@ -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");
});
});
+2
View File
@@ -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,
+86
View File
@@ -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<string, string | undefined>): 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<string, unknown>;
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<string, string[]>();
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 ");
}