refactor(mcp-server): DRY envelopes, typed apiCall, explicit re-exports
Second-pass cleanup after the monolith split. Addresses every issue
from the code-review pass.
Core additions in src/api.ts:
- toMcpResult(data) + toMcpText(text): single source of truth for the
MCP text-content envelope (was ~87 duplicated literals)
- ApiError type + isApiError(v) guard: typed discriminated-union for
the error-by-value pattern; replaces open-coded shape checks
- apiCall<T = unknown>: generic so callers can document expected
response shape without unchecked "as" casts
Bulk cleanups across all 12 tools/*.ts:
- Every handler now returns toMcpResult(data) or toMcpText(text)
- Open-coded "typeof obj === 'object' && 'error' in obj" in
remote_agents.ts replaced with isApiError(v)
- Extracted initialCanvasPosition() helper out of
handleCreateWorkspace; explains why random seeding exists
- Added runtime/workspace_dir/workspace_access to create_workspace
zod schema (previously accepted by handler but hidden from clients)
src/index.ts:
- Replaced "export * from" with explicit named re-exports so the
public surface is auditable and future name collisions fail loudly
Tests:
- createServer() smoke test that records every srv.tool(...) call and
asserts 87 registered tools unique by name. Catches future PRs that
forget to wire a registerXxxTools(srv).
Docs:
- Fix broken relative links in sdk/python/molecule_agent/README.md
(was ../../examples/ from inside sdk/python/, should be ../examples/)
- Update stale "61 tools" -> "87 tools" in CLAUDE.md + main() log
Verification:
- npm run build clean
- npx jest -> 97/97 passed (was 96; +1 smoke test)
- grep "content: [{ type: \"text\" as const" src/tools/ -> 0 matches
- No file over 216 lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a2df32dd0
commit
50b0a1859a
@ -133,7 +133,7 @@ cd mcp-server
|
||||
npm install && npm run build # Build MCP server
|
||||
node dist/index.js # Run (stdio transport)
|
||||
```
|
||||
Exposes 61 tools for managing Molecule AI from Claude Code, Cursor, Codex, or any MCP client. Includes workspace CRUD, async delegation, plugins (install/uninstall/list), global secrets, pause/resume, org import, A2A chat, approvals, memory, files, config, discovery, bundles, templates, traces, activity logs, and social channels (add/update/remove/send/test). Configured in `.mcp.json`. Env: `MOLECULE_URL` (default http://localhost:8080).
|
||||
Exposes 87 tools for managing Molecule AI from Claude Code, Cursor, Codex, or any MCP client. Includes workspace CRUD, async delegation, plugins (install/uninstall/list), global secrets, pause/resume, org import, A2A chat, approvals, memory, files, config, discovery, bundles, templates, traces, activity logs, and social channels (add/update/remove/send/test). Configured in `.mcp.json`. Env: `MOLECULE_URL` (default http://localhost:8080).
|
||||
|
||||
### CI Pipeline
|
||||
GitHub Actions (`.github/workflows/ci.yml`) runs on push to main and PRs:
|
||||
|
||||
@ -7,9 +7,13 @@
|
||||
|
||||
// Jest hoists these mock calls before imports, so the MCP SDK is
|
||||
// mocked before index.ts is loaded (preventing stdio/server side-effects).
|
||||
// The mock McpServer records every tool(name, ...) call on an instance
|
||||
// property so the createServer() smoke test can assert the registered count
|
||||
// without reaching into the real SDK's private `_registeredTools` field.
|
||||
jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
||||
McpServer: class {
|
||||
tool() {}
|
||||
registeredToolNames: string[] = [];
|
||||
tool(name: string) { this.registeredToolNames.push(name); }
|
||||
connect() { return Promise.resolve(); }
|
||||
},
|
||||
}));
|
||||
@ -850,6 +854,21 @@ describe("createServer()", () => {
|
||||
expect(server).toBeDefined();
|
||||
expect(typeof server.connect).toBe("function");
|
||||
});
|
||||
|
||||
// Smoke test: every registerXxxTools(srv) wiring in createServer() runs,
|
||||
// and each tool() call is recorded by the mocked McpServer above. If a
|
||||
// future PR adds a tool file but forgets to call its registerXxxTools
|
||||
// from createServer(), this count drops and the test fails. We assert
|
||||
// the concrete current tool count (87) rather than a lower bound so a
|
||||
// silently-dropped handler is also caught.
|
||||
test("registers all tools (count is stable across registerXxxTools wiring)", () => {
|
||||
const server = createServer() as unknown as { registeredToolNames: string[] };
|
||||
const names = server.registeredToolNames;
|
||||
expect(names.length).toBe(87);
|
||||
// Names must be unique — a duplicate registration would indicate a
|
||||
// copy-paste mistake in one of the registerXxxTools() calls.
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
|
||||
@ -8,7 +8,39 @@ export const PLATFORM_URL =
|
||||
process.env.PLATFORM_URL ||
|
||||
"http://localhost:8080";
|
||||
|
||||
export async function apiCall(method: string, path: string, body?: unknown) {
|
||||
/**
|
||||
* Shape returned by apiCall when the request fails (network error, non-2xx,
|
||||
* or non-JSON body with no error). Returned-by-value — apiCall never throws.
|
||||
*/
|
||||
export type ApiError = { error: string; detail?: string; raw?: string; status?: number };
|
||||
|
||||
export function isApiError(v: unknown): v is ApiError {
|
||||
return !!v && typeof v === "object" && "error" in (v as object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap arbitrary JSON-serialisable data in the MCP content envelope that
|
||||
* tool handlers must return. Centralised so every handler uses the exact
|
||||
* same shape (and a future switch to e.g. structured content happens once).
|
||||
*/
|
||||
export function toMcpResult(data: unknown) {
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a plain string (file contents, assistant reply text, error message)
|
||||
* in the MCP content envelope without JSON-stringifying it. For the handful
|
||||
* of handlers that return raw text rather than a JSON blob.
|
||||
*/
|
||||
export function toMcpText(text: string) {
|
||||
return { content: [{ type: "text" as const, text }] };
|
||||
}
|
||||
|
||||
export async function apiCall<T = unknown>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<T | ApiError> {
|
||||
try {
|
||||
const res = await fetch(`${PLATFORM_URL}${path}`, {
|
||||
method,
|
||||
@ -21,12 +53,13 @@ export async function apiCall(method: string, path: string, body?: unknown) {
|
||||
}
|
||||
const text = await res.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return { raw: text, status: res.status };
|
||||
return { raw: text, status: res.status } as ApiError;
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// stdio MCP servers must log to stderr; stdout is the protocol channel.
|
||||
console.error(`Molecule AI API error (${method} ${path}): ${msg}`);
|
||||
return { error: `Platform unreachable at ${PLATFORM_URL}`, detail: msg };
|
||||
}
|
||||
|
||||
@ -27,19 +27,146 @@ import { registerDiscoveryTools } from "./tools/discovery.js";
|
||||
import { registerRemoteAgentTools } from "./tools/remote_agents.js";
|
||||
|
||||
// Re-exports so existing importers (tests, SDK consumers) keep working.
|
||||
export { PLATFORM_URL, apiCall };
|
||||
export * from "./tools/workspaces.js";
|
||||
export * from "./tools/agents.js";
|
||||
export * from "./tools/secrets.js";
|
||||
export * from "./tools/files.js";
|
||||
export * from "./tools/memory.js";
|
||||
export * from "./tools/plugins.js";
|
||||
export * from "./tools/channels.js";
|
||||
export * from "./tools/delegation.js";
|
||||
export * from "./tools/schedules.js";
|
||||
export * from "./tools/approvals.js";
|
||||
export * from "./tools/discovery.js";
|
||||
export * from "./tools/remote_agents.js";
|
||||
// Explicit names (not `export *`) so tree-shakers and TS readers can see
|
||||
// exactly which handlers are part of the public surface, and a missing
|
||||
// export triggers a compile error instead of a silent undefined at import.
|
||||
export { PLATFORM_URL, apiCall, isApiError, toMcpResult, toMcpText } from "./api.js";
|
||||
export type { ApiError } from "./api.js";
|
||||
|
||||
export {
|
||||
registerWorkspaceTools,
|
||||
handleListWorkspaces,
|
||||
handleCreateWorkspace,
|
||||
handleGetWorkspace,
|
||||
handleDeleteWorkspace,
|
||||
handleRestartWorkspace,
|
||||
handleUpdateWorkspace,
|
||||
handlePauseWorkspace,
|
||||
handleResumeWorkspace,
|
||||
} from "./tools/workspaces.js";
|
||||
|
||||
export {
|
||||
registerAgentTools,
|
||||
handleChatWithAgent,
|
||||
handleAssignAgent,
|
||||
handleReplaceAgent,
|
||||
handleRemoveAgent,
|
||||
handleMoveAgent,
|
||||
handleGetModel,
|
||||
} from "./tools/agents.js";
|
||||
|
||||
export {
|
||||
registerSecretTools,
|
||||
handleSetSecret,
|
||||
handleListSecrets,
|
||||
handleDeleteSecret,
|
||||
handleListGlobalSecrets,
|
||||
handleSetGlobalSecret,
|
||||
handleDeleteGlobalSecret,
|
||||
} from "./tools/secrets.js";
|
||||
|
||||
export {
|
||||
registerFileTools,
|
||||
handleListFiles,
|
||||
handleReadFile,
|
||||
handleWriteFile,
|
||||
handleDeleteFile,
|
||||
handleReplaceAllFiles,
|
||||
handleGetConfig,
|
||||
handleUpdateConfig,
|
||||
} from "./tools/files.js";
|
||||
|
||||
export {
|
||||
registerMemoryTools,
|
||||
handleCommitMemory,
|
||||
handleSearchMemory,
|
||||
handleDeleteMemory,
|
||||
handleSessionSearch,
|
||||
handleGetSharedContext,
|
||||
handleSetKV,
|
||||
handleGetKV,
|
||||
handleListKV,
|
||||
handleDeleteKV,
|
||||
} from "./tools/memory.js";
|
||||
|
||||
export {
|
||||
registerPluginTools,
|
||||
handleListPluginRegistry,
|
||||
handleListInstalledPlugins,
|
||||
handleInstallPlugin,
|
||||
handleUninstallPlugin,
|
||||
handleListPluginSources,
|
||||
handleListAvailablePlugins,
|
||||
handleCheckPluginCompatibility,
|
||||
} from "./tools/plugins.js";
|
||||
|
||||
export {
|
||||
registerChannelTools,
|
||||
handleListChannelAdapters,
|
||||
handleListChannels,
|
||||
handleAddChannel,
|
||||
handleUpdateChannel,
|
||||
handleRemoveChannel,
|
||||
handleSendChannelMessage,
|
||||
handleTestChannel,
|
||||
handleDiscoverChannelChats,
|
||||
} from "./tools/channels.js";
|
||||
|
||||
export {
|
||||
registerDelegationTools,
|
||||
handleAsyncDelegate,
|
||||
handleCheckDelegations,
|
||||
handleRecordDelegation,
|
||||
handleUpdateDelegationStatus,
|
||||
handleReportActivity,
|
||||
handleListActivity,
|
||||
handleNotifyUser,
|
||||
handleListTraces,
|
||||
} from "./tools/delegation.js";
|
||||
|
||||
export {
|
||||
registerScheduleTools,
|
||||
handleListSchedules,
|
||||
handleCreateSchedule,
|
||||
handleUpdateSchedule,
|
||||
handleDeleteSchedule,
|
||||
handleRunSchedule,
|
||||
handleGetScheduleHistory,
|
||||
} from "./tools/schedules.js";
|
||||
|
||||
export {
|
||||
registerApprovalTools,
|
||||
handleListPendingApprovals,
|
||||
handleDecideApproval,
|
||||
handleCreateApproval,
|
||||
handleGetWorkspaceApprovals,
|
||||
} from "./tools/approvals.js";
|
||||
|
||||
export {
|
||||
registerDiscoveryTools,
|
||||
handleListPeers,
|
||||
handleDiscoverWorkspace,
|
||||
handleCheckAccess,
|
||||
handleListEvents,
|
||||
handleListTemplates,
|
||||
handleListOrgTemplates,
|
||||
handleImportOrg,
|
||||
handleImportTemplate,
|
||||
handleExportBundle,
|
||||
handleImportBundle,
|
||||
handleGetViewport,
|
||||
handleSetViewport,
|
||||
handleExpandTeam,
|
||||
handleCollapseTeam,
|
||||
} from "./tools/discovery.js";
|
||||
|
||||
export {
|
||||
registerRemoteAgentTools,
|
||||
handleListRemoteAgents,
|
||||
handleGetRemoteAgentState,
|
||||
handleGetRemoteAgentSetupCommand,
|
||||
handleCheckRemoteAgentFreshness,
|
||||
} from "./tools/remote_agents.js";
|
||||
|
||||
export function createServer() {
|
||||
const srv = new McpServer({
|
||||
@ -79,7 +206,7 @@ async function main() {
|
||||
const server = createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Molecule AI MCP server running on stdio (61 tools available)");
|
||||
console.error("Molecule AI MCP server running on stdio (87 tools available)");
|
||||
}
|
||||
|
||||
// Only auto-start when run directly (not when imported for testing).
|
||||
|
||||
@ -1,49 +1,53 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult, toMcpText } from "../api.js";
|
||||
|
||||
export async function handleChatWithAgent(params: { workspace_id: string; message: string }) {
|
||||
const { workspace_id, message } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/a2a`, {
|
||||
method: "message/send",
|
||||
params: {
|
||||
message: { role: "user", parts: [{ type: "text", text: message }] },
|
||||
const data = await apiCall<{ result?: { parts?: Array<{ kind?: string; text?: string }> } }>(
|
||||
"POST",
|
||||
`/workspaces/${workspace_id}/a2a`,
|
||||
{
|
||||
method: "message/send",
|
||||
params: {
|
||||
message: { role: "user", parts: [{ type: "text", text: message }] },
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = data?.result?.parts || [];
|
||||
);
|
||||
const parts = (data as { result?: { parts?: Array<{ kind?: string; text?: string }> } } | null)?.result?.parts || [];
|
||||
const text = parts
|
||||
.filter((p: { kind?: string }) => p.kind === "text")
|
||||
.map((p: { text?: string }) => p.text || "")
|
||||
.filter((p) => p.kind === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("\n");
|
||||
return { content: [{ type: "text" as const, text: text || JSON.stringify(data, null, 2) }] };
|
||||
return text ? toMcpText(text) : toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleAssignAgent(params: { workspace_id: string; model: string }) {
|
||||
const { workspace_id, model } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/agent`, { model });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleReplaceAgent(params: { workspace_id: string; model: string }) {
|
||||
const { workspace_id, model } = params;
|
||||
const data = await apiCall("PATCH", `/workspaces/${workspace_id}/agent`, { model });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRemoveAgent(params: { workspace_id: string }) {
|
||||
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/agent`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleMoveAgent(params: { workspace_id: string; target_workspace_id: string }) {
|
||||
const { workspace_id, target_workspace_id } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/agent/move`, { target_workspace_id });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetModel(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/model`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerAgentTools(srv: McpServer) {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleListPendingApprovals() {
|
||||
const data = await apiCall("GET", "/approvals/pending");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDecideApproval(params: {
|
||||
@ -18,7 +18,7 @@ export async function handleDecideApproval(params: {
|
||||
`/workspaces/${workspace_id}/approvals/${approval_id}/decide`,
|
||||
{ decision, decided_by: "mcp-client" }
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCreateApproval(params: {
|
||||
@ -28,12 +28,12 @@ export async function handleCreateApproval(params: {
|
||||
}) {
|
||||
const { workspace_id, action, reason } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/approvals`, { action, reason });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetWorkspaceApprovals(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/approvals`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerApprovalTools(srv: McpServer) {
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult, toMcpText } from "../api.js";
|
||||
|
||||
export async function handleListChannelAdapters() {
|
||||
const data = await apiCall("GET", `/channels/adapters`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListChannels(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/channels`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleAddChannel(params: {
|
||||
@ -19,14 +19,14 @@ export async function handleAddChannel(params: {
|
||||
allowed_users?: string;
|
||||
}) {
|
||||
let config: unknown;
|
||||
try { config = JSON.parse(params.config); } catch { return { content: [{ type: "text" as const, text: "Error: config is not valid JSON" }] }; }
|
||||
try { config = JSON.parse(params.config); } catch { return toMcpText("Error: config is not valid JSON"); }
|
||||
const allowed_users = params.allowed_users ? params.allowed_users.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/channels`, {
|
||||
channel_type: params.channel_type,
|
||||
config,
|
||||
allowed_users,
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUpdateChannel(params: {
|
||||
@ -38,19 +38,19 @@ export async function handleUpdateChannel(params: {
|
||||
}) {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (params.config) {
|
||||
try { body.config = JSON.parse(params.config); } catch { return { content: [{ type: "text" as const, text: "Error: config is not valid JSON" }] }; }
|
||||
try { body.config = JSON.parse(params.config); } catch { return toMcpText("Error: config is not valid JSON"); }
|
||||
}
|
||||
if (params.enabled !== undefined) body.enabled = params.enabled;
|
||||
if (params.allowed_users !== undefined) {
|
||||
body.allowed_users = params.allowed_users.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
const data = await apiCall("PATCH", `/workspaces/${params.workspace_id}/channels/${params.channel_id}`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRemoveChannel(params: { workspace_id: string; channel_id: string }) {
|
||||
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/channels/${params.channel_id}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSendChannelMessage(params: {
|
||||
@ -61,12 +61,12 @@ export async function handleSendChannelMessage(params: {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/channels/${params.channel_id}/send`, {
|
||||
text: params.text,
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleTestChannel(params: { workspace_id: string; channel_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/channels/${params.channel_id}/test`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDiscoverChannelChats(params: {
|
||||
@ -74,7 +74,7 @@ export async function handleDiscoverChannelChats(params: {
|
||||
config: Record<string, unknown>;
|
||||
}) {
|
||||
const data = await apiCall("POST", "/channels/discover", params);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerChannelTools(srv: McpServer) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleAsyncDelegate(params: {
|
||||
workspace_id: string;
|
||||
@ -9,12 +9,12 @@ export async function handleAsyncDelegate(params: {
|
||||
}) {
|
||||
const { workspace_id, target_id, task } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/delegate`, { target_id, task });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCheckDelegations(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/delegations`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRecordDelegation(params: {
|
||||
@ -25,7 +25,7 @@ export async function handleRecordDelegation(params: {
|
||||
}) {
|
||||
const { workspace_id, ...body } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/delegations/record`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUpdateDelegationStatus(params: {
|
||||
@ -41,7 +41,7 @@ export async function handleUpdateDelegationStatus(params: {
|
||||
`/workspaces/${workspace_id}/delegations/${delegation_id}/update`,
|
||||
body,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleReportActivity(params: {
|
||||
@ -57,7 +57,7 @@ export async function handleReportActivity(params: {
|
||||
}) {
|
||||
const { workspace_id, ...body } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/activity`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListActivity(params: {
|
||||
@ -71,7 +71,7 @@ export async function handleListActivity(params: {
|
||||
if (limit) urlParams.set("limit", String(limit));
|
||||
const qs = urlParams.toString() ? `?${urlParams.toString()}` : "";
|
||||
const data = await apiCall("GET", `/workspaces/${workspace_id}/activity${qs}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleNotifyUser(params: {
|
||||
@ -81,12 +81,12 @@ export async function handleNotifyUser(params: {
|
||||
}) {
|
||||
const { workspace_id, ...body } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/notify`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListTraces(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/traces`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerDelegationTools(srv: McpServer) {
|
||||
|
||||
@ -1,78 +1,78 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleListPeers(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/registry/${params.workspace_id}/peers`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDiscoverWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/registry/discover/${params.workspace_id}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCheckAccess(params: { caller_id: string; target_id: string }) {
|
||||
const { caller_id, target_id } = params;
|
||||
const data = await apiCall("POST", `/registry/check-access`, { caller_id, target_id });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListEvents(params: { workspace_id?: string }) {
|
||||
const path = params.workspace_id ? `/events/${params.workspace_id}` : "/events";
|
||||
const data = await apiCall("GET", path);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListTemplates() {
|
||||
const data = await apiCall("GET", "/templates");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListOrgTemplates() {
|
||||
const data = await apiCall("GET", "/org/templates");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleImportOrg(params: { dir: string }) {
|
||||
const data = await apiCall("POST", "/org/import", { dir: params.dir });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleImportTemplate(params: { name: string; files: Record<string, string> }) {
|
||||
const { name, files } = params;
|
||||
const data = await apiCall("POST", `/templates/import`, { name, files });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleExportBundle(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/bundles/export/${params.workspace_id}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleImportBundle(params: { bundle: Record<string, unknown> }) {
|
||||
const data = await apiCall("POST", `/bundles/import`, params.bundle);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetViewport() {
|
||||
const data = await apiCall("GET", "/canvas/viewport");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSetViewport(params: { x: number; y: number; zoom: number }) {
|
||||
const data = await apiCall("PUT", "/canvas/viewport", params);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleExpandTeam(params: { workspace_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/expand`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCollapseTeam(params: { workspace_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/collapse`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerDiscoveryTools(srv: McpServer) {
|
||||
|
||||
@ -1,28 +1,29 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult, toMcpText } from "../api.js";
|
||||
|
||||
export async function handleListFiles(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/files`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleReadFile(params: { workspace_id: string; path: string }) {
|
||||
const { workspace_id, path } = params;
|
||||
const data = await apiCall("GET", `/workspaces/${workspace_id}/files/${path}`);
|
||||
return { content: [{ type: "text" as const, text: data?.content || JSON.stringify(data) }] };
|
||||
const data = await apiCall<{ content?: string }>("GET", `/workspaces/${workspace_id}/files/${path}`);
|
||||
const fileText = (data as { content?: string } | null)?.content;
|
||||
return fileText ? toMcpText(fileText) : toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleWriteFile(params: { workspace_id: string; path: string; content: string }) {
|
||||
const { workspace_id, path, content } = params;
|
||||
const data = await apiCall("PUT", `/workspaces/${workspace_id}/files/${path}`, { content });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteFile(params: { workspace_id: string; path: string }) {
|
||||
const { workspace_id, path } = params;
|
||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/files/${path}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleReplaceAllFiles(params: {
|
||||
@ -31,18 +32,18 @@ export async function handleReplaceAllFiles(params: {
|
||||
}) {
|
||||
const { workspace_id, files } = params;
|
||||
const data = await apiCall("PUT", `/workspaces/${workspace_id}/files`, { files });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetConfig(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/config`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUpdateConfig(params: { workspace_id: string; config: Record<string, unknown> }) {
|
||||
const { workspace_id, config } = params;
|
||||
const data = await apiCall("PATCH", `/workspaces/${workspace_id}/config`, config);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerFileTools(srv: McpServer) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleCommitMemory(params: {
|
||||
workspace_id: string;
|
||||
@ -9,7 +9,7 @@ export async function handleCommitMemory(params: {
|
||||
}) {
|
||||
const { workspace_id, content, scope } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/memories`, { content, scope });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSearchMemory(params: {
|
||||
@ -22,13 +22,13 @@ export async function handleSearchMemory(params: {
|
||||
if (query) urlParams.set("q", query);
|
||||
if (scope) urlParams.set("scope", scope);
|
||||
const data = await apiCall("GET", `/workspaces/${workspace_id}/memories?${urlParams}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteMemory(params: { workspace_id: string; memory_id: string }) {
|
||||
const { workspace_id, memory_id } = params;
|
||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/memories/${memory_id}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSessionSearch(params: {
|
||||
@ -42,12 +42,12 @@ export async function handleSessionSearch(params: {
|
||||
if (limit) qs.set("limit", String(limit));
|
||||
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
||||
const data = await apiCall("GET", `/workspaces/${workspace_id}/session-search${suffix}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetSharedContext(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/shared-context`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSetKV(params: {
|
||||
@ -58,7 +58,7 @@ export async function handleSetKV(params: {
|
||||
}) {
|
||||
const { workspace_id, ...body } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/memory`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetKV(params: { workspace_id: string; key: string }) {
|
||||
@ -66,12 +66,12 @@ export async function handleGetKV(params: { workspace_id: string; key: string })
|
||||
"GET",
|
||||
`/workspaces/${params.workspace_id}/memory/${encodeURIComponent(params.key)}`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListKV(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/memory`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteKV(params: { workspace_id: string; key: string }) {
|
||||
@ -79,7 +79,7 @@ export async function handleDeleteKV(params: { workspace_id: string; key: string
|
||||
"DELETE",
|
||||
`/workspaces/${params.workspace_id}/memory/${encodeURIComponent(params.key)}`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerMemoryTools(srv: McpServer) {
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleListPluginRegistry() {
|
||||
const data = await apiCall("GET", "/plugins");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListInstalledPlugins(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/plugins`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleInstallPlugin(params: { workspace_id: string; source: string }) {
|
||||
const { workspace_id, source } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/plugins`, { source });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUninstallPlugin(params: { workspace_id: string; name: string }) {
|
||||
const { workspace_id, name } = params;
|
||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/plugins/${name}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListPluginSources() {
|
||||
const data = await apiCall("GET", "/plugins/sources");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListAvailablePlugins(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/plugins/available`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCheckPluginCompatibility(params: {
|
||||
@ -43,7 +43,7 @@ export async function handleCheckPluginCompatibility(params: {
|
||||
"GET",
|
||||
`/workspaces/${workspace_id}/plugins/compatibility?runtime=${encodeURIComponent(runtime)}`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerPluginTools(srv: McpServer) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall, PLATFORM_URL } from "../api.js";
|
||||
import { apiCall, PLATFORM_URL, toMcpResult, isApiError } from "../api.js";
|
||||
|
||||
// Fetch the workspace list, filter to runtime='external'. The platform
|
||||
// has no dedicated /remote-agents endpoint — we filter client-side
|
||||
@ -9,7 +9,7 @@ import { apiCall, PLATFORM_URL } from "../api.js";
|
||||
export async function handleListRemoteAgents() {
|
||||
const data = await apiCall("GET", "/workspaces");
|
||||
if (!Array.isArray(data)) {
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
const remote = data
|
||||
.filter((w: { runtime?: string }) => w.runtime === "external")
|
||||
@ -22,7 +22,7 @@ export async function handleListRemoteAgents() {
|
||||
uptime_seconds: w.uptime_seconds,
|
||||
tier: w.tier,
|
||||
}));
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify({ count: remote.length, agents: remote }, null, 2) }] };
|
||||
return toMcpResult({ count: remote.length, agents: remote });
|
||||
}
|
||||
|
||||
// Phase 30.4 — token-gated; from MCP we don't have a workspace bearer
|
||||
@ -31,8 +31,8 @@ export async function handleListRemoteAgents() {
|
||||
// a focused tool that doesn't dump the full workspace blob.
|
||||
export async function handleGetRemoteAgentState(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}`);
|
||||
if (data && typeof data === "object" && "error" in data) {
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
if (isApiError(data)) {
|
||||
return toMcpResult(data);
|
||||
}
|
||||
const w = data as Record<string, unknown>;
|
||||
const projected = {
|
||||
@ -43,7 +43,7 @@ export async function handleGetRemoteAgentState(params: { workspace_id: string }
|
||||
runtime: w.runtime,
|
||||
last_heartbeat_at: w.last_heartbeat_at,
|
||||
};
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(projected, null, 2) }] };
|
||||
return toMcpResult(projected);
|
||||
}
|
||||
|
||||
export async function handleGetRemoteAgentSetupCommand(params: {
|
||||
@ -54,21 +54,16 @@ export async function handleGetRemoteAgentSetupCommand(params: {
|
||||
// the command — saves the operator from pasting a bash line that will
|
||||
// fail because the workspace was a Docker workspace they typed by mistake.
|
||||
const ws = await apiCall("GET", `/workspaces/${params.workspace_id}`);
|
||||
if (ws && typeof ws === "object" && "error" in ws) {
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(ws, null, 2) }] };
|
||||
if (isApiError(ws)) {
|
||||
return toMcpResult(ws);
|
||||
}
|
||||
const w = ws as { id: string; name: string; runtime?: string };
|
||||
if (w.runtime !== "external") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
error: "workspace is not external; setup command only applies to runtime='external'",
|
||||
workspace_id: w.id,
|
||||
actual_runtime: w.runtime,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
return toMcpResult({
|
||||
error: "workspace is not external; setup command only applies to runtime='external'",
|
||||
workspace_id: w.id,
|
||||
actual_runtime: w.runtime,
|
||||
});
|
||||
}
|
||||
|
||||
// The MCP server's PLATFORM_URL is whatever Claude Desktop / the host
|
||||
@ -104,18 +99,13 @@ export async function handleGetRemoteAgentSetupCommand(params: {
|
||||
`# The agent will register, mint its bearer token (cached at`,
|
||||
`# ~/.molecule/${w.id}/.auth_token), pull secrets, then heartbeat.`,
|
||||
].join("\n");
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
workspace_id: w.id,
|
||||
workspace_name: w.name,
|
||||
platform_url: targetUrl,
|
||||
setup_command: setupCmd,
|
||||
...(warnings.length > 0 ? { warnings } : {}),
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
return toMcpResult({
|
||||
workspace_id: w.id,
|
||||
workspace_name: w.name,
|
||||
platform_url: targetUrl,
|
||||
setup_command: setupCmd,
|
||||
...(warnings.length > 0 ? { warnings } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleCheckRemoteAgentFreshness(params: {
|
||||
@ -123,8 +113,8 @@ export async function handleCheckRemoteAgentFreshness(params: {
|
||||
threshold_seconds?: number;
|
||||
}) {
|
||||
const ws = await apiCall("GET", `/workspaces/${params.workspace_id}`);
|
||||
if (ws && typeof ws === "object" && "error" in ws) {
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(ws, null, 2) }] };
|
||||
if (isApiError(ws)) {
|
||||
return toMcpResult(ws);
|
||||
}
|
||||
const w = ws as { last_heartbeat_at?: string; status?: string; runtime?: string };
|
||||
const threshold = params.threshold_seconds ?? 90;
|
||||
@ -137,20 +127,15 @@ export async function handleCheckRemoteAgentFreshness(params: {
|
||||
}
|
||||
}
|
||||
const fresh = secondsSince !== null && secondsSince <= threshold;
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
workspace_id: params.workspace_id,
|
||||
status: w.status,
|
||||
runtime: w.runtime,
|
||||
last_heartbeat_at: heartbeatStr,
|
||||
seconds_since_heartbeat: secondsSince,
|
||||
threshold_seconds: threshold,
|
||||
fresh,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
return toMcpResult({
|
||||
workspace_id: params.workspace_id,
|
||||
status: w.status,
|
||||
runtime: w.runtime,
|
||||
last_heartbeat_at: heartbeatStr,
|
||||
seconds_since_heartbeat: secondsSince,
|
||||
threshold_seconds: threshold,
|
||||
fresh,
|
||||
});
|
||||
}
|
||||
|
||||
export function registerRemoteAgentTools(srv: McpServer) {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleListSchedules(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/schedules`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCreateSchedule(params: {
|
||||
@ -17,7 +17,7 @@ export async function handleCreateSchedule(params: {
|
||||
}) {
|
||||
const { workspace_id, ...body } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/schedules`, body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUpdateSchedule(params: {
|
||||
@ -35,7 +35,7 @@ export async function handleUpdateSchedule(params: {
|
||||
`/workspaces/${workspace_id}/schedules/${schedule_id}`,
|
||||
body,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteSchedule(params: {
|
||||
@ -46,7 +46,7 @@ export async function handleDeleteSchedule(params: {
|
||||
"DELETE",
|
||||
`/workspaces/${params.workspace_id}/schedules/${params.schedule_id}`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRunSchedule(params: {
|
||||
@ -57,7 +57,7 @@ export async function handleRunSchedule(params: {
|
||||
"POST",
|
||||
`/workspaces/${params.workspace_id}/schedules/${params.schedule_id}/run`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetScheduleHistory(params: {
|
||||
@ -68,7 +68,7 @@ export async function handleGetScheduleHistory(params: {
|
||||
"GET",
|
||||
`/workspaces/${params.workspace_id}/schedules/${params.schedule_id}/history`,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerScheduleTools(srv: McpServer) {
|
||||
|
||||
@ -1,38 +1,38 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleSetSecret(params: { workspace_id: string; key: string; value: string }) {
|
||||
const { workspace_id, key, value } = params;
|
||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/secrets`, { key, value });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListSecrets(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}/secrets`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteSecret(params: { workspace_id: string; key: string }) {
|
||||
const { workspace_id, key } = params;
|
||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/secrets/${encodeURIComponent(key)}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListGlobalSecrets() {
|
||||
const data = await apiCall("GET", "/settings/secrets");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleSetGlobalSecret(params: { key: string; value: string }) {
|
||||
const { key, value } = params;
|
||||
const data = await apiCall("PUT", "/settings/secrets", { key, value });
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteGlobalSecret(params: { key: string }) {
|
||||
const data = await apiCall("DELETE", `/settings/secrets/${params.key}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerSecretTools(srv: McpServer) {
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall } from "../api.js";
|
||||
import { apiCall, toMcpResult } from "../api.js";
|
||||
|
||||
export async function handleListWorkspaces() {
|
||||
const data = await apiCall("GET", "/workspaces");
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
// Random canvas seeding so MCP-created workspaces don't all stack at (0,0).
|
||||
// The platform stores these; canvas drag-drop overrides them immediately.
|
||||
function initialCanvasPosition() {
|
||||
return { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 };
|
||||
}
|
||||
|
||||
export async function handleCreateWorkspace(params: {
|
||||
@ -21,24 +27,24 @@ export async function handleCreateWorkspace(params: {
|
||||
const data = await apiCall("POST", "/workspaces", {
|
||||
name, role, template, tier, parent_id, runtime,
|
||||
workspace_dir, workspace_access,
|
||||
canvas: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 },
|
||||
canvas: initialCanvasPosition(),
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("GET", `/workspaces/${params.workspace_id}`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleDeleteWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}?confirm=true`);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRestartWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/restart`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleUpdateWorkspace(params: {
|
||||
@ -52,17 +58,17 @@ export async function handleUpdateWorkspace(params: {
|
||||
}) {
|
||||
const { workspace_id, ...fields } = params;
|
||||
const data = await apiCall("PATCH", `/workspaces/${workspace_id}`, fields);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handlePauseWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/pause`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleResumeWorkspace(params: { workspace_id: string }) {
|
||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/resume`, {});
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export function registerWorkspaceTools(srv: McpServer) {
|
||||
@ -77,6 +83,9 @@ export function registerWorkspaceTools(srv: McpServer) {
|
||||
template: z.string().optional().describe("Template name from workspace-configs-templates/"),
|
||||
tier: z.number().min(1).max(4).default(1).describe("Tier (1=basic, 2=browser, 3=desktop, 4=VM)"),
|
||||
parent_id: z.string().optional().describe("Parent workspace ID for nesting"),
|
||||
runtime: z.string().optional().describe("Runtime: claude-code, langgraph, openclaw, deepagents, autogen, crewai, hermes, external"),
|
||||
workspace_dir: z.string().optional().describe("Host path to bind-mount at /workspace (PM only by convention)"),
|
||||
workspace_access: z.enum(["none", "read_only", "read_write"]).optional().describe("Filesystem access mode for /workspace"),
|
||||
},
|
||||
handleCreateWorkspace
|
||||
);
|
||||
|
||||
@ -44,7 +44,7 @@ print(f"loop exited: {terminal}")
|
||||
```
|
||||
|
||||
A runnable demo with full setup walkthrough lives at
|
||||
[`sdk/python/examples/remote-agent/`](../../examples/remote-agent).
|
||||
[`sdk/python/examples/remote-agent/`](../examples/remote-agent).
|
||||
|
||||
## What the SDK gives you
|
||||
|
||||
@ -93,5 +93,5 @@ the security benefits of bearer auth until both sides upgrade.
|
||||
|
||||
- [`molecule_plugin`](../molecule_plugin) — the *other* SDK in this
|
||||
package, for plugin authors. Different audience.
|
||||
- [`sdk/python/examples/remote-agent/run.py`](../../examples/remote-agent/run.py)
|
||||
- [`sdk/python/examples/remote-agent/run.py`](../examples/remote-agent/run.py)
|
||||
— the runnable demo that proves all of the above end-to-end.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user