molecule-core/mcp-server/src/api.ts
Hongming Wang af931aa8da 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>
2026-04-13 14:26:17 -07:00

67 lines
2.4 KiB
TypeScript

// Prefer MOLECULE_URL (the canonical MCP env var), fall back to PLATFORM_URL
// (what the workspace runtime already injects for heartbeat/register), and
// only then to localhost:8080. Injecting MOLECULE_URL at container provision
// is handled by platform/internal/provisioner/provisioner.go; this fallback
// chain protects older containers and host-side users alike. Fixes #67.
export const PLATFORM_URL =
process.env.MOLECULE_URL ||
process.env.PLATFORM_URL ||
"http://localhost:8080";
/**
* 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,
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const text = await res.text();
return { error: `HTTP ${res.status}`, detail: text };
}
const text = await res.text();
try {
return JSON.parse(text) as T;
} catch {
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 };
}
}