statemap-mcp 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +123 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { readFileSync } from "node:fs";
5
+ import { readFileSync, writeFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  function loadConfig() {
8
8
  const apiKey = process.env.STATEMAP_API_KEY;
@@ -27,6 +27,33 @@ function loadConfig() {
27
27
  }
28
28
  return { projectId, baseUrl: baseUrl.replace(/\/$/, ""), apiKey };
29
29
  }
30
+ async function apiRaw(baseUrl, apiKey, path, opts) {
31
+ const url = `${baseUrl}${path}`;
32
+ const res = await fetch(url, {
33
+ ...opts,
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ "Authorization": `Bearer ${apiKey}`,
37
+ "X-Agent-Id": "mcp-server",
38
+ ...opts?.headers,
39
+ },
40
+ });
41
+ const body = await res.text();
42
+ if (!res.ok) {
43
+ let msg = res.statusText;
44
+ try {
45
+ msg = JSON.parse(body).error || msg;
46
+ }
47
+ catch { }
48
+ throw new Error(`API error ${res.status}: ${msg}`);
49
+ }
50
+ try {
51
+ return JSON.parse(body);
52
+ }
53
+ catch {
54
+ return body;
55
+ }
56
+ }
30
57
  async function api(cfg, path, opts) {
31
58
  const err = checkConfig();
32
59
  if (err)
@@ -254,28 +281,53 @@ function batchSearch(snap, op, names) {
254
281
  }
255
282
  return results;
256
283
  }
257
- const loaded = loadConfig();
258
- const configError = "error" in loaded ? loaded.error : null;
259
- const config = "error" in loaded ? { projectId: "", baseUrl: "", apiKey: "" } : loaded;
284
+ let loaded = loadConfig();
285
+ let configError = "error" in loaded ? loaded.error : null;
286
+ let config = "error" in loaded ? { projectId: "", baseUrl: "", apiKey: "" } : loaded;
260
287
  const server = new McpServer({
261
288
  name: "statemap",
262
- version: "0.1.5",
289
+ version: "0.1.6",
290
+ }, {
291
+ instructions: `Statemap is a system state model for this project. Here's how to get started:
292
+
293
+ 1. If there's no .statemap config file in the project root, run statemap_init first:
294
+ - Use action "list" to see existing projects
295
+ - Use action "select" to connect to one, or "create" to make a new one
296
+ - This writes a .statemap file and configures the server automatically
297
+
298
+ 2. Once connected, the typical workflow is:
299
+ - statemap_snapshot: Get an overview of all declared state (models, containers, transitions, invariants, read deps)
300
+ - statemap_list: Drill into a specific entity type (supports pagination with offset/limit)
301
+ - statemap_validate: Check invariant constraints for violations
302
+ - statemap_divergence: Detect drift between declared state and actual code
303
+ - statemap_import: Bulk upsert entities (idempotent — safe to re-run)
304
+ - statemap_analyze: Submit source files for codebase analysis
305
+ - statemap_architect_review: Start a guided review session with the project architect to identify dead and unhooked state
306
+
307
+ 3. When modifying state, use the specific update tools (statemap_update_model, statemap_update_container, etc.)
308
+
309
+ If any tool returns a config error, run statemap_init to fix it.`,
263
310
  });
264
311
  function checkConfig() {
265
312
  return configError;
266
313
  }
314
+ function reloadConfig() {
315
+ loaded = loadConfig();
316
+ configError = "error" in loaded ? loaded.error : null;
317
+ config = "error" in loaded ? { projectId: "", baseUrl: "", apiKey: "" } : loaded;
318
+ }
267
319
  server.tool("statemap_snapshot", "Load the system state model. Returns a summary with all model names, fields, containers, transitions, invariants, and read deps. Use statemap_list for full details on a specific entity type.", async () => {
268
320
  const data = await api(config, "/snapshot");
269
321
  return text(summarizeSnapshot(data));
270
322
  });
271
- server.tool("statemap_list", "List full details for a specific entity type: models, containers, transitions, invariants, or read-deps. Use after statemap_snapshot to drill into specifics.", {
323
+ server.tool("statemap_list", "List full details for a specific entity type: models, containers, transitions, invariants, or read-deps. Use after statemap_snapshot to drill into specifics. Supports pagination via offset (default 0) and limit (default 50).", {
272
324
  entity_type: z.enum(["models", "containers", "transitions", "invariants", "read-deps"]).describe("Which entity type to list"),
273
- }, async ({ entity_type }) => {
325
+ offset: z.number().int().min(0).default(0).describe("Starting index for pagination (default 0)"),
326
+ limit: z.number().int().min(1).max(100).default(50).describe("Number of items to return (default 50, max 100)"),
327
+ }, async ({ entity_type, offset, limit }) => {
274
328
  const data = await api(config, `/${entity_type}`);
275
- if (data.length > 50) {
276
- return text({ total: data.length, showing: 50, truncated: true, items: data.slice(0, 50) });
277
- }
278
- return text(data);
329
+ const page = data.slice(offset, offset + limit);
330
+ return text({ total: data.length, offset, limit, showing: page.length, hasMore: offset + limit < data.length, items: page });
279
331
  });
280
332
  server.tool("statemap_validate", "Check all invariant constraints — returns summary counts and up to 30 failures with details", async () => {
281
333
  const data = await api(config, "/validate");
@@ -453,6 +505,14 @@ ${modelList}
453
505
 
454
506
  Follow these steps IN ORDER. Do not skip entity types — dead state accumulates everywhere.
455
507
 
508
+ ### Step 0: Safe checkpoint
509
+ **⚠️ WARNING: This review can result in changes to your codebase, database schemas, and live infrastructure. These changes can be extremely dangerous if handled incorrectly. Always create a rollback point before proceeding.**
510
+
511
+ Check if the project is a git repo. If it is, ask the user: **"Can I commit your current work-in-progress so we have a safe rollback point before the review?"** If they agree, create a commit with a message like "wip: checkpoint before architect review". If there's no git repo, suggest initializing one with \`git init\` and an initial commit before proceeding. If the user declines, make sure they understand the risk before continuing.
512
+
513
+ ### Step 0.5: Codebase discovery
514
+ **Deploy a subagent (if possible)** to scan the codebase and build context before the interactive review begins. The agent should find schema files (schema.sql, prisma, drizzle, etc), state stores (zustand, redux, context), route handlers, components, and config files. This context is essential for Steps 9-10 when you'll be making actual code changes. Keep the results to yourself — don't dump raw findings on the architect.
515
+
456
516
  ### Step 1: Present overview
457
517
  Summarize the health from the data above. Note total error counts but don't try to fix anything yet — many errors are downstream of dead state.
458
518
 
@@ -486,20 +546,27 @@ Don't assume — a dead parent doesn't always mean dead children.
486
546
  After all kills are identified across all entity types, confirm: **"Everything remaining is still live and intentional?"**
487
547
 
488
548
  ### Step 9: Clean up the codebase
489
- For each dead entity, find and remove the actual code. Search the codebase for:
549
+ **Deploy a single sequential cleanup subagent.** Do NOT parallelize removals — a single bad delete can cascade into broken imports and runtime errors. The agent processes each dead entity ONE AT A TIME, in this order for each:
550
+
551
+ 1. Search by entity name (grep for the model/table/store name) to find ALL references
552
+ 2. Present the full list of files and lines that will be affected to the architect
553
+ 3. Wait for explicit approval before making changes
554
+ 4. Make the removal
555
+ 5. Check imports — if removing code breaks imports elsewhere, trace and fix those too
556
+ 6. Confirm the build/typecheck still passes before moving to the next entity
557
+
558
+ What to remove per entity type:
490
559
  - **Dead models**: DROP/remove from schema files (schema.sql, prisma, drizzle, etc), delete TypeScript interfaces/types, remove API routes/handlers that CRUD this model, remove store slices/hooks that manage this state, remove UI components that only exist to display this model
491
560
  - **Dead containers**: remove store definitions (zustand stores, context providers, cache configs), remove connection/initialization code
492
561
  - **Dead transitions**: remove the handler/route/function that performs this mutation, remove associated validation logic
493
562
  - **Dead read deps**: remove the component/hook/function that consumes this state
494
563
  - **Dead invariants**: remove validation checks that enforce the dead constraint
495
564
 
496
- Search by entity name (grep for the model/table/store name) to find all references. Check imports if removing a file breaks imports elsewhere, trace those too.
497
-
498
- After the code is cleaned, re-import to Statemap using statemap_import so the model reflects the new codebase reality.
565
+ After ALL dead entities are cleaned, re-import to Statemap using statemap_import so the model reflects the new codebase reality.
499
566
  Re-run statemap_validate and statemap_divergence to confirm improvement.
500
567
 
501
568
  ### Step 10: Fix the living
502
- Address remaining errors on live entities: missing relationships, orphaned references, unhooked state. Propose fixes based on code analysis, grouped by confidence:
569
+ **Deploy a subagent** to address remaining errors on live entities: missing relationships, orphaned references, unhooked state. The agent proposes fixes based on code analysis, grouped by confidence:
503
570
  - **Obvious wiring** — FK exists in schema but undeclared as a relationship, field exists but not in model, etc. Present these as a batch.
504
571
  - **Inferred connections** — looks related but no explicit FK, probable container assignment based on naming, etc. Present these individually.
505
572
  Ask the architect to approve: they can rubber-stamp the obvious stuff ("all good") and cherry-pick the ambiguous ones.
@@ -517,5 +584,45 @@ Ask the architect to approve: they can rubber-stamp the obvious stuff ("all good
517
584
  Start by presenting the overview, then present the model list. Ask the architect which models are dead.`;
518
585
  return text(procedure);
519
586
  });
587
+ server.tool("statemap_init", "Initialize Statemap for this project. Lists your existing projects or creates a new one, then writes a .statemap config file. Run this if you get config errors or when setting up Statemap for the first time.", {
588
+ action: z.enum(["list", "create", "select"]).describe("'list' to see existing projects, 'create' to make a new one, 'select' to pick an existing one by ID"),
589
+ project_name: z.string().optional().describe("Name for new project (required for 'create')"),
590
+ project_id: z.string().optional().describe("Project ID to select (required for 'select')"),
591
+ }, async ({ action, project_name, project_id }) => {
592
+ const apiKey = process.env.STATEMAP_API_KEY;
593
+ if (!apiKey) {
594
+ return text({ error: "STATEMAP_API_KEY environment variable is required. Get your key at https://statemap.io/app" });
595
+ }
596
+ const baseUrl = (process.env.STATEMAP_BASE_URL || "https://statemap.io").replace(/\/$/, "");
597
+ if (action === "list") {
598
+ const projects = await apiRaw(baseUrl, apiKey, "/api/projects");
599
+ if (projects.length === 0) {
600
+ return text({ projects: [], hint: "No projects found. Use action 'create' with a project_name to create one." });
601
+ }
602
+ return text({ projects, hint: "Use action 'select' with a project_id to connect to one of these, or 'create' to make a new one." });
603
+ }
604
+ if (action === "create") {
605
+ if (!project_name)
606
+ return text({ error: "project_name is required for 'create'" });
607
+ const project = await apiRaw(baseUrl, apiKey, "/api/projects", {
608
+ method: "POST",
609
+ body: JSON.stringify({ name: project_name }),
610
+ });
611
+ const configPath = resolve(process.cwd(), ".statemap");
612
+ writeFileSync(configPath, JSON.stringify({ project_id: project.id }, null, 2) + "\n");
613
+ reloadConfig();
614
+ return text({ ok: true, project, config_written: configPath });
615
+ }
616
+ if (action === "select") {
617
+ if (!project_id)
618
+ return text({ error: "project_id is required for 'select'" });
619
+ const project = await apiRaw(baseUrl, apiKey, `/api/projects/${project_id}`);
620
+ const configPath = resolve(process.cwd(), ".statemap");
621
+ writeFileSync(configPath, JSON.stringify({ project_id: project.id }, null, 2) + "\n");
622
+ reloadConfig();
623
+ return text({ ok: true, project, config_written: configPath });
624
+ }
625
+ return text({ error: "Unknown action" });
626
+ });
520
627
  const transport = new StdioServerTransport();
521
628
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statemap-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "MCP server for Statemap — system state working memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",