@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/README.md +152 -0
- package/dist/cli.js +276 -0
- package/dist/cloudClient.js +71 -0
- package/dist/cloudOperators.js +530 -0
- package/dist/cloudTools.js +1727 -0
- package/dist/cloudTypes.js +8 -0
- package/dist/credentials.js +97 -0
- package/dist/index.js +294 -0
- package/dist/llm/client.js +155 -0
- package/dist/llm/cost.js +65 -0
- package/dist/operators/asit.js +50 -0
- package/dist/operators/combine.js +10 -0
- package/dist/operators/contradiction.js +13 -0
- package/dist/operators/explore.js +14 -0
- package/dist/operators/triz-matrix.js +1964 -0
- package/dist/operators/triz.js +23 -0
- package/dist/operators/types.js +10 -0
- package/dist/prompt/compose.js +141 -0
- package/dist/prompt/parse.js +99 -0
- package/dist/prompt/schema.js +30 -0
- package/dist/realtime.js +192 -0
- package/dist/store.js +128 -0
- package/dist/tools.js +264 -0
- package/dist/types.js +5 -0
- package/dist/zod-to-json-schema.js +89 -0
- package/package.json +54 -0
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,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
|
+
}
|