@heuresis/mcp 1.0.0-rc.1

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.
package/dist/tools.js ADDED
@@ -0,0 +1,264 @@
1
+ // Read-only tools exposed by the heuresis-mcp v0 server.
2
+ //
3
+ // All tools are designed to be cheap, deterministic, and side-effect free.
4
+ // Agent-write tools (append_concept, set_parent, …) are intentionally
5
+ // deferred — they need a provenance + write-back path through the running
6
+ // app, which is real Phase 12 work.
7
+ import { z } from 'zod';
8
+ const MAX_RESULTS = 50;
9
+ // Bulk tool results are folded into the agent's next API request. Oversized
10
+ // payloads get reset in transit by strict corporate proxies (ECONNRESET), so
11
+ // bulk tools default to a compact view and the agent opts into full text per
12
+ // node via get_concept.
13
+ const detailArg = z
14
+ .enum(['compact', 'full'])
15
+ .default('compact')
16
+ .describe("'compact' drops description/rationale to keep the payload small; 'full' includes them.");
17
+ /** A flat, agent-friendly view of a Node (drops position/embedding). */
18
+ function nodeView(n, detail = 'full') {
19
+ const base = {
20
+ id: n.id,
21
+ label: n.label,
22
+ status: n.status,
23
+ starred: n.starred,
24
+ parentId: n.parentId,
25
+ operator: n.operator?.family,
26
+ partitionAttribute: n.partitionAttribute,
27
+ tags: n.tags,
28
+ updatedAt: new Date(n.updatedAt).toISOString(),
29
+ };
30
+ if (detail === 'compact')
31
+ return base;
32
+ return { ...base, description: n.description, rationale: n.rationale };
33
+ }
34
+ // ── get_workspace_summary ───────────────────────────────────────────────────
35
+ export const getWorkspaceSummaryInput = z.object({}).strict();
36
+ export async function getWorkspaceSummary(store) {
37
+ const [workspaces, nodes, edges, projects, ideas] = await Promise.all([
38
+ store.workspaces(),
39
+ store.nodes(),
40
+ store.edges(),
41
+ store.projects(),
42
+ store.ideas(),
43
+ ]);
44
+ return {
45
+ snapshotPath: store.getSnapshotPath(),
46
+ workspaces: workspaces.map((w) => ({ id: w.id, name: w.name })),
47
+ counts: {
48
+ nodes: nodes.length,
49
+ edges: edges.length,
50
+ projects: projects.length,
51
+ ideas: ideas.length,
52
+ },
53
+ projects: projects.map((p) => ({
54
+ id: p.id,
55
+ name: p.name,
56
+ brief: p.brief,
57
+ direction: p.direction,
58
+ lifecycle: p.lifecycle,
59
+ nodeCount: p.nodeIds.length,
60
+ rootNodeId: p.rootNodeId,
61
+ })),
62
+ ideas: ideas.map((i) => ({
63
+ id: i.id,
64
+ name: i.name,
65
+ nodeCount: i.nodeIds.length,
66
+ color: i.color,
67
+ })),
68
+ };
69
+ }
70
+ // ── search_concepts ─────────────────────────────────────────────────────────
71
+ export const searchConceptsInput = z
72
+ .object({
73
+ query: z.string().describe('Substring matched against label, description, tags, partitionAttribute.'),
74
+ limit: z.number().int().min(1).max(MAX_RESULTS).default(20),
75
+ projectId: z.string().optional().describe('Restrict results to nodes belonging to this project.'),
76
+ status: z.enum(['open', 'validated', 'archived']).optional(),
77
+ detail: detailArg,
78
+ })
79
+ .strict();
80
+ export async function searchConcepts(store, args) {
81
+ const q = args.query.toLowerCase().trim();
82
+ const allNodes = await store.nodes();
83
+ const project = args.projectId ? await store.projectById(args.projectId) : null;
84
+ const allowed = project ? new Set(project.nodeIds) : null;
85
+ const hits = allNodes
86
+ .filter((n) => {
87
+ if (allowed && !allowed.has(n.id))
88
+ return false;
89
+ if (args.status && n.status !== args.status)
90
+ return false;
91
+ if (!q)
92
+ return true;
93
+ const hay = [
94
+ n.label,
95
+ n.description,
96
+ n.partitionAttribute ?? '',
97
+ n.rationale ?? '',
98
+ (n.tags ?? []).join(' '),
99
+ ]
100
+ .join(' ')
101
+ .toLowerCase();
102
+ return hay.includes(q);
103
+ })
104
+ .sort((a, b) => b.updatedAt - a.updatedAt)
105
+ .slice(0, args.limit)
106
+ .map((n) => nodeView(n, args.detail));
107
+ return { query: q, total: hits.length, detail: args.detail, results: hits };
108
+ }
109
+ // ── get_concept ─────────────────────────────────────────────────────────────
110
+ export const getConceptInput = z
111
+ .object({
112
+ id: z.string(),
113
+ includeAncestry: z.boolean().default(true),
114
+ includeChildren: z.boolean().default(true),
115
+ includeIdeaMemberships: z.boolean().default(true),
116
+ })
117
+ .strict();
118
+ export async function getConcept(store, args) {
119
+ const node = await store.nodeById(args.id);
120
+ if (!node)
121
+ return { error: `No concept with id ${args.id}` };
122
+ const out = { node: nodeView(node) };
123
+ if (args.includeAncestry) {
124
+ const chain = [];
125
+ const seen = new Set();
126
+ let cur = node.parentId;
127
+ while (cur && !seen.has(cur)) {
128
+ seen.add(cur);
129
+ const p = await store.nodeById(cur);
130
+ if (!p)
131
+ break;
132
+ chain.unshift({ id: p.id, label: p.label });
133
+ cur = p.parentId;
134
+ }
135
+ out.ancestry = chain;
136
+ }
137
+ if (args.includeChildren) {
138
+ const kids = await store.childrenOf(node.id);
139
+ out.children = kids.map((k) => ({ id: k.id, label: k.label, status: k.status }));
140
+ }
141
+ if (args.includeIdeaMemberships) {
142
+ const ideas = await store.ideas();
143
+ out.ideaMemberships = ideas
144
+ .filter((i) => i.nodeIds.includes(node.id))
145
+ .map((i) => ({ id: i.id, name: i.name, color: i.color }));
146
+ }
147
+ return out;
148
+ }
149
+ // ── get_subtree ─────────────────────────────────────────────────────────────
150
+ export const getSubtreeInput = z
151
+ .object({
152
+ rootId: z.string(),
153
+ depth: z.number().int().min(0).max(6).default(3).describe('How many generations below the root to include.'),
154
+ detail: detailArg,
155
+ })
156
+ .strict();
157
+ export async function getSubtree(store, args) {
158
+ const root = await store.nodeById(args.rootId);
159
+ if (!root)
160
+ return { error: `No concept with id ${args.rootId}` };
161
+ const nodes = await store.descendantsOf(args.rootId, args.depth);
162
+ return {
163
+ rootId: args.rootId,
164
+ depth: args.depth,
165
+ detail: args.detail,
166
+ nodeCount: nodes.length,
167
+ nodes: nodes.map((n) => nodeView(n, args.detail)),
168
+ };
169
+ }
170
+ // ── list_projects ───────────────────────────────────────────────────────────
171
+ export const listProjectsInput = z.object({}).strict();
172
+ export async function listProjects(store) {
173
+ const projects = await store.projects();
174
+ return {
175
+ projects: projects.map((p) => ({
176
+ id: p.id,
177
+ name: p.name,
178
+ brief: p.brief,
179
+ direction: p.direction,
180
+ lifecycle: p.lifecycle,
181
+ nodeCount: p.nodeIds.length,
182
+ rootNodeId: p.rootNodeId,
183
+ })),
184
+ };
185
+ }
186
+ // ── get_project_graph ───────────────────────────────────────────────────────
187
+ export const getProjectGraphInput = z
188
+ .object({
189
+ projectId: z.string(),
190
+ includeArchived: z.boolean().default(false),
191
+ detail: detailArg,
192
+ })
193
+ .strict();
194
+ export async function getProjectGraph(store, args) {
195
+ const project = await store.projectById(args.projectId);
196
+ if (!project)
197
+ return { error: `No project with id ${args.projectId}` };
198
+ const memberIds = new Set(project.nodeIds);
199
+ const [allNodes, allEdges] = await Promise.all([store.nodes(), store.edges()]);
200
+ const nodes = allNodes
201
+ .filter((n) => memberIds.has(n.id))
202
+ .filter((n) => (args.includeArchived ? true : n.status !== 'archived'));
203
+ const nodeIdSet = new Set(nodes.map((n) => n.id));
204
+ const edges = allEdges
205
+ .filter((e) => nodeIdSet.has(e.fromId) && nodeIdSet.has(e.toId))
206
+ .map((e) => ({ from: e.fromId, to: e.toId, kind: e.kind }));
207
+ return {
208
+ project: {
209
+ id: project.id,
210
+ name: project.name,
211
+ brief: project.brief,
212
+ direction: project.direction,
213
+ lifecycle: project.lifecycle,
214
+ rootNodeId: project.rootNodeId,
215
+ },
216
+ detail: args.detail,
217
+ nodeCount: nodes.length,
218
+ edgeCount: edges.length,
219
+ nodes: nodes.map((n) => nodeView(n, args.detail)),
220
+ edges,
221
+ };
222
+ }
223
+ // ── list_recent_decisions ───────────────────────────────────────────────────
224
+ export const listRecentDecisionsInput = z
225
+ .object({
226
+ sinceMs: z
227
+ .number()
228
+ .int()
229
+ .optional()
230
+ .describe('Unix-ms cutoff; default = last 7 days.'),
231
+ limit: z.number().int().min(1).max(MAX_RESULTS).default(20),
232
+ })
233
+ .strict();
234
+ export async function listRecentDecisions(store, args) {
235
+ const cutoff = args.sinceMs ?? Date.now() - 7 * 24 * 60 * 60 * 1000;
236
+ const allNodes = await store.nodes();
237
+ // "Decisions" = validated, starred, or just-archived nodes — the ones
238
+ // the user has explicitly resolved one way or the other.
239
+ const decisions = allNodes
240
+ .filter((n) => n.updatedAt >= cutoff &&
241
+ (n.starred || n.status === 'validated' || n.status === 'archived'))
242
+ .sort((a, b) => b.updatedAt - a.updatedAt)
243
+ .slice(0, args.limit)
244
+ .map((n) => ({
245
+ id: n.id,
246
+ label: n.label,
247
+ status: n.status,
248
+ starred: n.starred,
249
+ updatedAt: new Date(n.updatedAt).toISOString(),
250
+ decision: n.status === 'validated'
251
+ ? 'validated'
252
+ : n.status === 'archived'
253
+ ? 'archived'
254
+ : n.starred
255
+ ? 'starred'
256
+ : 'open',
257
+ }));
258
+ return {
259
+ sinceMs: cutoff,
260
+ since: new Date(cutoff).toISOString(),
261
+ count: decisions.length,
262
+ decisions,
263
+ };
264
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // Mirror of the relevant subset of src/types/domain.ts from the main app.
2
+ // Kept inline rather than imported across packages so this server is a
3
+ // standalone npm package — drop into any agent setup without the rest of
4
+ // the Heuresis monorepo.
5
+ export {};
@@ -0,0 +1,89 @@
1
+ // Minimal zod → JSON-Schema converter. We only support the shapes our
2
+ // tool inputs use (object roots with string/number/boolean/enum/optional/
3
+ // default leaves + a description). This avoids pulling in the heavy
4
+ // `zod-to-json-schema` npm package for a couple-hundred-byte job.
5
+ import { z } from 'zod';
6
+ function leafSchema(schema) {
7
+ // Peel optional/default/describe wrappers, recording metadata as we go.
8
+ let cur = schema;
9
+ let description;
10
+ let defaultValue;
11
+ // Capture `.describe(...)` text from any wrapper level.
12
+ if (cur.description) {
13
+ description = cur.description;
14
+ }
15
+ while (cur instanceof z.ZodOptional ||
16
+ cur instanceof z.ZodDefault ||
17
+ cur instanceof z.ZodNullable) {
18
+ if (cur instanceof z.ZodDefault) {
19
+ defaultValue = cur._def.defaultValue();
20
+ }
21
+ cur = cur._def.innerType ?? cur;
22
+ if (!description &&
23
+ cur.description) {
24
+ description = cur.description;
25
+ }
26
+ }
27
+ const out = {};
28
+ if (cur instanceof z.ZodString)
29
+ out.type = 'string';
30
+ else if (cur instanceof z.ZodNumber) {
31
+ out.type = 'number';
32
+ // z.number().int() — there's no clean API, but checks live on _def.checks.
33
+ const checks = cur._def
34
+ .checks;
35
+ if (checks) {
36
+ for (const c of checks) {
37
+ if (c.kind === 'int')
38
+ out.type = 'integer';
39
+ if (c.kind === 'min' && typeof c.value === 'number')
40
+ out.minimum = c.value;
41
+ if (c.kind === 'max' && typeof c.value === 'number')
42
+ out.maximum = c.value;
43
+ }
44
+ }
45
+ }
46
+ else if (cur instanceof z.ZodBoolean)
47
+ out.type = 'boolean';
48
+ else if (cur instanceof z.ZodEnum) {
49
+ out.type = 'string';
50
+ out.enum = [...cur.options];
51
+ }
52
+ else if (cur instanceof z.ZodLiteral) {
53
+ out.enum = [cur.value];
54
+ }
55
+ else if (cur instanceof z.ZodArray) {
56
+ out.type = 'array';
57
+ }
58
+ else if (cur instanceof z.ZodObject) {
59
+ // Nested object — recurse via the public path.
60
+ return zodToJsonSchema(cur);
61
+ }
62
+ else {
63
+ // Unknown / unsupported — fall back to "any".
64
+ }
65
+ if (description)
66
+ out.description = description;
67
+ if (defaultValue !== undefined)
68
+ out.default = defaultValue;
69
+ return out;
70
+ }
71
+ export function zodToJsonSchema(schema) {
72
+ const shape = schema.shape;
73
+ const properties = {};
74
+ const required = [];
75
+ for (const [key, value] of Object.entries(shape)) {
76
+ properties[key] = leafSchema(value);
77
+ const isOptional = value instanceof z.ZodOptional || value instanceof z.ZodDefault;
78
+ if (!isOptional)
79
+ required.push(key);
80
+ }
81
+ const out = {
82
+ type: 'object',
83
+ properties,
84
+ additionalProperties: false,
85
+ };
86
+ if (required.length > 0)
87
+ out.required = required;
88
+ return out;
89
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@heuresis/mcp",
3
+ "version": "1.0.0-rc.1",
4
+ "description": "Cloud-authenticated Model Context Protocol server for a Heuresis workspace. Logs into the user's Heuresis account and lets any MCP client (Claude Desktop, Claude Code, Cursor, custom agents) read and write the same workspace the webapp uses. 31 data tools, 3 operator tools (Branch/Matrix/C-K/ASIT/TRIZ/Free/Combine/Explore), and live Realtime change subscriptions.",
5
+ "type": "module",
6
+ "bin": {
7
+ "heuresis-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsc --watch",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "homepage": "https://heuresis.app/mcp",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ToremLabs/Heuresis.git",
26
+ "directory": "mcp-server"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "heuresis",
32
+ "ideation",
33
+ "knowledge-graph",
34
+ "claude-code",
35
+ "claude-desktop",
36
+ "cursor"
37
+ ],
38
+ "dependencies": {
39
+ "@anthropic-ai/sdk": "^0.40.0",
40
+ "@google/generative-ai": "^0.21.0",
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "@supabase/supabase-js": "^2.45.0",
43
+ "openai": "^4.71.0",
44
+ "zod": "^3.23.0"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.6.0",
48
+ "@types/node": "^22.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "license": "AGPL-3.0-or-later"
54
+ }