-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
The Chronova MCP server is a thin, read-only bridge between an MCP-capable AI client and the Chronova HTTP API. It does not persist data or mutate any state on Chronova.
AI client (Claude Desktop / Cursor / OpenCode / HTTP)
│
│ stdio OR HTTP (Streamable HTTP transport)
▼
┌─────────────────────────────────────────────────────┐
│ src/index.ts (HTTP entrypoint) │
│ src/stdio.ts (stdio entrypoint, npm bin) │
│ parses CLI flags, resolves config │
│ │ │
│ ▼ │
│ src/server.ts createApp() / startServer() │
│ Express app, /health, /mcp endpoint │
│ per-session McpServer + StreamableHTTPServerTransport │
│ │ │
│ ▼ │
│ src/tools/index.ts registerAllTools(server, chronova)│
│ imports and calls every registerXxx tool │
│ │ │
│ ▼ │
│ src/tools/* registerXxx(server, chronova) │
│ zod inputSchema → chronova.get() → JSON text │
│ │ │
│ ▼ │
│ src/lib/chronova-client.ts ChronovaClient.get() │
│ fetch + Bearer auth + 30s timeout + error map │
│ │ │
│ ▼ │
│ Chronova HTTP API (https://chronova.dev/api/v1) │
└─────────────────────────────────────────────────────┘
Two independent entrypoints share the same tool registrations and the same ChronovaClient:
-
src/index.ts— HTTP entrypoint.parseArgs()translates--port/--api-url/--helpintoprocess.env, then callsstartServer()fromserver.ts. This is whatnpm startand the Docker image run. -
src/stdio.ts— stdio entrypoint and the publishedchronova-mcp-serverbin (package.json#bin). It creates anMcpServerconnected toStdioServerTransportand exits with an error if no API key is resolvable (HTTP entrypoint only warns).
Both call resolveConfig() (src/lib/config.ts) and construct a ChronovaClient. Tool registration is centralized in src/tools/index.ts via registerAllTools(server, chronova), so every tool is wired once for both transports.
src/server.ts exposes:
-
createApp(config?)— builds an Express app with CORS and JSON body parsing.GET /healthreturns{ status, version }.POST|GET|DELETE /mcphandle MCP protocol traffic via the Streamable HTTP transport. -
startServer()— resolves config, warns on missing API key, listens onconfig.port, wiresSIGTERM/SIGINTgraceful shutdown.
Sessions are managed in an in-memory Map<sessionId, Session>. Each new session (no mcp-session-id header) creates a fresh McpServer + StreamableHTTPServerTransport pair with a random UUID generator and stores it on onsessioninitialized. server.onclose deletes the session. The MCP server version is loaded from package.json by src/version.ts and imported into both entrypoints, so the version reported by initialize and /health stays in sync with the published npm package version.
Each tool lives in its own src/tools/<tool-name>.ts and exports a registerXxx(server: McpServer, chronova: ChronovaClient) function. src/tools/index.ts exports registerAllTools(server, chronova), which imports every tool registrar and calls it in one place. Both entrypoints call registerAllTools so new tools only need to be added to src/tools/index.ts, not to src/server.ts and src/stdio.ts individually.
Inside each registrar, server.registerTool(name, { description, inputSchema: z.object(...), annotations: { readOnlyHint: true } }, async (args) => {...}) registers it. All four tools are read-only.
Each handler:
- Builds a Chronova API path and query params from the zod-parsed args.
- Calls
chronova.get<{ data: T }>(path, params). - Returns
{ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }on success. - Catches any error and delegates to
formatToolError(error)(src/lib/errors.ts), which returns{ content: [{ type: "text", text }], isError: true }— usingerror.messagefor aChronovaApiError, or"Unexpected error: ..."for anything else.
See Tools reference for per-tool paths, schemas, and response shapes.
src/lib/chronova-client.ts — ChronovaClient:
- Constructor normalizes
baseUrlto end with/sonew URL(path, baseUrl)resolves correctly (without the trailing slash,new URL("users/current", "https://host/api/v1")yieldshttps://host/users/current). -
get<T>(path, params?)builds the URL, omits empty/undefined params, setsAuthorization: Bearer <key>andAccept: application/json, and usesAbortSignal.timeout(30_000). - HTTP errors are mapped via
mapHttpStatusToErrorand network/abort errors viamapNetworkError(see Errors & status mapping).
src/lib/types.ts holds the response interfaces for each Chronova endpoint:
-
ChronovaUser— profile, subscription,github_connected, organizations. -
ChronovaStatsRange— totals plus arrays of language/project/editor/OS breakdowns,daily_stats,hourly_stats,best_day. -
ChronovaHeartbeat/ChronovaHeartbeatResponse— raw activity events with pagination metadata. -
ChronovaAiAnalytics— adoption timeline, contribution share, with/without-AI comparison, language matrix, project dependency, efficiency trend.
These types describe the Chronova API contract as the server understands it; they are the source of truth for tool response shapes.
| Path | Role |
|---|---|
src/index.ts |
HTTP entrypoint, CLI arg parsing, --help
|
src/stdio.ts |
stdio entrypoint (npm bin) |
src/server.ts |
Express app, /health, /mcp, session lifecycle |
src/lib/config.ts |
Config resolution (env → ~/.chronova.cfg → ~/.wakatime.cfg) |
src/lib/chronova-client.ts |
HTTP client wrapper around fetch
|
src/lib/errors.ts |
ChronovaApiError + status/network mappers + formatToolError
|
src/lib/types.ts |
Chronova response type definitions |
src/version.ts |
Reads package.json#version at import time; shared by both entrypoints |
src/tools/index.ts |
registerAllTools — central registrar that wires every tool into the server |
src/tools/*.ts |
One file per MCP tool, registerXxx pattern |
tests/helpers/mock-server.ts |
fetch mock + MCP-over-HTTP test harness |
tests/integration/*.test.ts |
Integration tests for server + tools |