statemap-mcp 0.1.0

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ function loadConfig() {
8
+ const apiKey = process.env.STATEMAP_API_KEY;
9
+ if (!apiKey) {
10
+ process.stderr.write("STATEMAP_API_KEY environment variable is required\n");
11
+ process.exit(1);
12
+ }
13
+ let projectId = process.env.STATEMAP_PROJECT_ID || "";
14
+ let baseUrl = process.env.STATEMAP_BASE_URL || "https://statemap.io";
15
+ try {
16
+ const configPath = resolve(process.cwd(), ".statemap");
17
+ const raw = readFileSync(configPath, "utf-8");
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed.project_id)
20
+ projectId = parsed.project_id;
21
+ if (parsed.base_url)
22
+ baseUrl = parsed.base_url;
23
+ }
24
+ catch {
25
+ // .statemap file not found — rely on env vars
26
+ }
27
+ if (!projectId) {
28
+ process.stderr.write("Project ID required: set in .statemap file or STATEMAP_PROJECT_ID env var\n");
29
+ process.exit(1);
30
+ }
31
+ return { projectId, baseUrl: baseUrl.replace(/\/$/, ""), apiKey };
32
+ }
33
+ async function api(config, path, opts) {
34
+ const url = `${config.baseUrl}/api/projects/${config.projectId}${path}`;
35
+ const res = await fetch(url, {
36
+ ...opts,
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ "Authorization": `Bearer ${config.apiKey}`,
40
+ "X-Agent-Id": "mcp-server",
41
+ ...opts?.headers,
42
+ },
43
+ });
44
+ const body = await res.text();
45
+ if (!res.ok) {
46
+ let msg = res.statusText;
47
+ try {
48
+ msg = JSON.parse(body).error || msg;
49
+ }
50
+ catch { }
51
+ throw new Error(`API error ${res.status}: ${msg}`);
52
+ }
53
+ try {
54
+ return JSON.parse(body);
55
+ }
56
+ catch {
57
+ return body;
58
+ }
59
+ }
60
+ function text(data) {
61
+ return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] };
62
+ }
63
+ const config = loadConfig();
64
+ const server = new McpServer({
65
+ name: "statemap",
66
+ version: "0.1.0",
67
+ });
68
+ server.tool("statemap_snapshot", "Load the full system state model — all models, containers, transitions, invariants, and read dependencies", async () => {
69
+ const data = await api(config, "/snapshot");
70
+ return text(data);
71
+ });
72
+ server.tool("statemap_query", "Ask a natural language question about the system state (e.g. 'what breaks if I remove the cart model?', 'who reads from user?')", { question: z.string().describe("Natural language question about the system state"), scope: z.array(z.string()).optional().describe("Optional: limit query to specific entity names") }, async ({ question, scope }) => {
73
+ const data = await api(config, "/query", { method: "POST", body: JSON.stringify({ question, scope }) });
74
+ return text(data);
75
+ });
76
+ server.tool("statemap_validate", "Check all invariant constraints pass — returns passed/failed counts and details for each failure", async () => {
77
+ const data = await api(config, "/validate");
78
+ return text(data);
79
+ });
80
+ server.tool("statemap_divergence", "Check for internal consistency issues — orphaned states, missing transitions, broken references", async () => {
81
+ const data = await api(config, "/divergence");
82
+ return text(data);
83
+ });
84
+ server.tool("statemap_update_model", "Create or update a data model declaration. Omit id to create, include id to update.", {
85
+ id: z.string().optional().describe("Model ID — omit to create new, include to update existing"),
86
+ name: z.string().describe("Model name (e.g. 'User', 'Order')"),
87
+ layer: z.enum(["database", "application", "ui"]).describe("Where this model lives"),
88
+ schema: z.object({
89
+ fields: z.array(z.object({
90
+ name: z.string(),
91
+ type: z.string(),
92
+ nullable: z.boolean().default(false),
93
+ unique: z.boolean().default(false),
94
+ default: z.string().optional(),
95
+ description: z.string().optional(),
96
+ })),
97
+ }).describe("Field definitions"),
98
+ relationships: z.array(z.object({
99
+ target_model_id: z.string().describe("Target model name or ID"),
100
+ type: z.enum(["one_to_one", "one_to_many", "many_to_many"]),
101
+ foreign_key: z.string().optional(),
102
+ description: z.string().optional(),
103
+ })).optional().describe("Relationships to other models"),
104
+ }, async ({ id, ...data }) => {
105
+ if (id) {
106
+ const result = await api(config, `/models/${id}`, { method: "PUT", body: JSON.stringify(data) });
107
+ return text(result);
108
+ }
109
+ const result = await api(config, "/models", { method: "POST", body: JSON.stringify(data) });
110
+ return text(result);
111
+ });
112
+ server.tool("statemap_update_container", "Create or update a state container (where state lives). Omit id to create, include id to update.", {
113
+ id: z.string().optional().describe("Container ID — omit to create, include to update"),
114
+ name: z.string().describe("Container name (e.g. 'PostgreSQL', 'Redis Cache', 'useAuthStore')"),
115
+ container_type: z.enum(["database", "cache", "local_state", "url_params", "session", "cookie", "env", "kv", "durable_object", "r2"]).describe("Container type"),
116
+ models: z.array(z.string()).optional().describe("Model names or IDs stored in this container"),
117
+ config: z.record(z.unknown()).optional().describe("Optional configuration metadata"),
118
+ }, async ({ id, ...data }) => {
119
+ if (id) {
120
+ const result = await api(config, `/containers/${id}`, { method: "PUT", body: JSON.stringify(data) });
121
+ return text(result);
122
+ }
123
+ const result = await api(config, "/containers", { method: "POST", body: JSON.stringify(data) });
124
+ return text(result);
125
+ });
126
+ server.tool("statemap_update_transition", "Create or update a state transition (mutation). Omit id to create, include id to update.", {
127
+ id: z.string().optional().describe("Transition ID — omit to create, include to update"),
128
+ name: z.string().describe("Transition name (e.g. 'CreateOrder', 'UpdateProfile')"),
129
+ trigger: z.string().describe("What triggers this (e.g. 'api_call', 'user_action', 'cron')"),
130
+ source_states: z.array(z.string()).describe("Model names read/modified by this transition"),
131
+ target_states: z.array(z.string()).describe("Model names written/created by this transition"),
132
+ preconditions: z.array(z.string()).optional().describe("Conditions that must be true before"),
133
+ postconditions: z.array(z.string()).optional().describe("Conditions guaranteed after"),
134
+ }, async ({ id, ...data }) => {
135
+ if (id) {
136
+ const result = await api(config, `/transitions/${id}`, { method: "PUT", body: JSON.stringify(data) });
137
+ return text(result);
138
+ }
139
+ const result = await api(config, "/transitions", { method: "POST", body: JSON.stringify(data) });
140
+ return text(result);
141
+ });
142
+ server.tool("statemap_add_invariant", "Add a constraint/invariant that the system must satisfy", {
143
+ name: z.string().describe("Invariant name (e.g. 'UserEmailUnique', 'OrderTotalPositive')"),
144
+ expression: z.string().describe("Constraint expression (e.g. 'unique(User.email)', 'Order.total >= 0')"),
145
+ scope: z.array(z.string()).optional().describe("Model names this invariant applies to"),
146
+ severity: z.enum(["error", "warning", "info"]).default("error").describe("How severe a violation is"),
147
+ }, async (data) => {
148
+ const result = await api(config, "/invariants", { method: "POST", body: JSON.stringify(data) });
149
+ return text(result);
150
+ });
151
+ server.tool("statemap_update_read_deps", "Create or update a read dependency (what consumes state). Omit id to create, include id to update.", {
152
+ id: z.string().optional().describe("Read dep ID — omit to create, include to update"),
153
+ consumer: z.string().describe("Consumer name (e.g. 'UserProfile', 'checkoutApi', 'useCartStore')"),
154
+ consumer_type: z.enum(["component", "view", "hook", "function", "api_route", "worker"]).describe("Consumer type"),
155
+ state_refs: z.array(z.string()).describe("Model names this consumer reads from"),
156
+ }, async ({ id, ...data }) => {
157
+ if (id) {
158
+ const result = await api(config, `/read-deps/${id}`, { method: "PUT", body: JSON.stringify(data) });
159
+ return text(result);
160
+ }
161
+ const result = await api(config, "/read-deps", { method: "POST", body: JSON.stringify(data) });
162
+ return text(result);
163
+ });
164
+ server.tool("statemap_import", "Bulk import entities — for initial project scan. Provide arrays of models, containers, transitions, invariants, and/or read dependencies.", {
165
+ data_models: z.array(z.record(z.unknown())).optional().describe("Array of model objects to import"),
166
+ state_containers: z.array(z.record(z.unknown())).optional().describe("Array of container objects to import"),
167
+ state_transitions: z.array(z.record(z.unknown())).optional().describe("Array of transition objects to import"),
168
+ invariants: z.array(z.record(z.unknown())).optional().describe("Array of invariant objects to import"),
169
+ read_dependencies: z.array(z.record(z.unknown())).optional().describe("Array of read dependency objects to import"),
170
+ }, async (data) => {
171
+ const result = await api(config, "/import", { method: "POST", body: JSON.stringify(data) });
172
+ return text(result);
173
+ });
174
+ server.tool("statemap_analyze", "Submit source files for codebase analysis and drift detection — compares actual code against declared model", {
175
+ files: z.array(z.object({
176
+ path: z.string().describe("File path relative to repo root"),
177
+ content: z.string().describe("File contents"),
178
+ })).describe("Source files to analyze (Prisma schemas, TypeScript interfaces, SQL, state stores, etc.)"),
179
+ }, async (data) => {
180
+ const result = await api(config, "/analyze-and-compare", { method: "POST", body: JSON.stringify(data) });
181
+ return text(result);
182
+ });
183
+ const transport = new StdioServerTransport();
184
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "statemap-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Statemap — system state working memory for AI agents",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "statemap-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "zod": "^3.25.67"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.15.0",
23
+ "typescript": "^5.8.3"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "statemap",
28
+ "ai",
29
+ "agent",
30
+ "state-management"
31
+ ],
32
+ "license": "MIT"
33
+ }