feat: export workspace target resolver #22
@@ -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
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ");
|
||||
}
|
||||
Reference in New Issue
Block a user