hunchful-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.
- package/README.md +78 -0
- package/index.mjs +136 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Hunchful MCP server
|
|
2
|
+
|
|
3
|
+
Connect your AI assistant directly to a [Hunchful](https://hunchful.io) model, so
|
|
4
|
+
it can **read** your collaboration model and **refine** it as it learns — instead
|
|
5
|
+
of you copy-pasting links or payloads.
|
|
6
|
+
|
|
7
|
+
It's a thin wrapper over Hunchful's public HTTP endpoints; no data is stored by
|
|
8
|
+
the server, and it honours the same provenance-pointer contract (no transcripts,
|
|
9
|
+
quotes, or third-party names).
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | What it does | Auth |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `read_model` | Read the model: hunches, confidence, refinable axis ids | none (public) |
|
|
16
|
+
| `propose_refinement` | Propose changes → land in the owner's confirm queue | contribution token |
|
|
17
|
+
| `apply_refinement` | Apply changes directly as one logged revision | edit token |
|
|
18
|
+
|
|
19
|
+
`propose_refinement` is the safe default: the owner confirms each change. Use
|
|
20
|
+
`apply_refinement` only when the owner has handed you their edit key.
|
|
21
|
+
|
|
22
|
+
## Configure
|
|
23
|
+
|
|
24
|
+
Set these environment variables (in your MCP client config):
|
|
25
|
+
|
|
26
|
+
- `HUNCHFUL_MODEL_ID` — your model id (from `hunchful.io/m/<id>`), so tools can omit it
|
|
27
|
+
- `HUNCHFUL_CONTRIBUTION_TOKEN` — scoped, propose-only key. Generate it on your model's
|
|
28
|
+
Edit page → **"let an agent contribute"**. It can only *propose*.
|
|
29
|
+
- `HUNCHFUL_EDIT_TOKEN` — the model's edit key. Enables **direct** apply. Optional and
|
|
30
|
+
powerful — only set it if you want the AI to write without a confirm step. You can
|
|
31
|
+
regenerate the key anytime to revoke access.
|
|
32
|
+
- `HUNCHFUL_BASE` — override the site origin (default `https://hunchful.io`).
|
|
33
|
+
|
|
34
|
+
### Claude Desktop / Claude Code (`claude_desktop_config.json` or `.mcp.json`)
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"hunchful": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "hunchful-mcp"],
|
|
42
|
+
"env": {
|
|
43
|
+
"HUNCHFUL_MODEL_ID": "your-model-id",
|
|
44
|
+
"HUNCHFUL_CONTRIBUTION_TOKEN": "ctb_…"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To run it from a local checkout instead of npx:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"hunchful": {
|
|
57
|
+
"command": "node",
|
|
58
|
+
"args": ["/absolute/path/to/hunchful/mcp-server/index.mjs"],
|
|
59
|
+
"env": { "HUNCHFUL_MODEL_ID": "your-model-id", "HUNCHFUL_CONTRIBUTION_TOKEN": "ctb_…" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Install (local)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd mcp-server
|
|
69
|
+
npm install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Safety
|
|
73
|
+
|
|
74
|
+
- The owner stays in control: `propose_refinement` never changes anything until the
|
|
75
|
+
owner confirms; a shared edit key can be regenerated to revoke access instantly.
|
|
76
|
+
- Provenance pointers only. The server rejects anything but `{patternId, direction,
|
|
77
|
+
observedAt, source}` — no free text.
|
|
78
|
+
- Every applied change is recorded in the model's revision history.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hunchful MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Lets an AI assistant connect directly to a Hunchful model — read it, and
|
|
6
|
+
* refine it — instead of the person copy-pasting links or payloads. It's a thin,
|
|
7
|
+
* dependency-light wrapper over Hunchful's public HTTP endpoints:
|
|
8
|
+
*
|
|
9
|
+
* read_model GET /m/{id}/cmp.json (open)
|
|
10
|
+
* propose_refinement POST /api/contribute (contribution token → owner confirms)
|
|
11
|
+
* apply_refinement POST /api/apply (edit token → direct, one logged revision)
|
|
12
|
+
*
|
|
13
|
+
* Configure with environment variables (in your MCP client config):
|
|
14
|
+
* HUNCHFUL_MODEL_ID default model id, so tools can omit it
|
|
15
|
+
* HUNCHFUL_CONTRIBUTION_TOKEN scoped, propose-only key (Edit page → "let an agent contribute")
|
|
16
|
+
* HUNCHFUL_EDIT_TOKEN the model's edit key — enables direct apply (powerful; optional)
|
|
17
|
+
* HUNCHFUL_BASE override the site origin (default https://hunchful.io)
|
|
18
|
+
*
|
|
19
|
+
* Provenance-pointer contract only: no transcripts, quotes, or third-party names.
|
|
20
|
+
*/
|
|
21
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
|
|
25
|
+
const BASE = (process.env.HUNCHFUL_BASE || "https://hunchful.io").replace(/\/+$/, "");
|
|
26
|
+
const DEFAULT_MODEL = process.env.HUNCHFUL_MODEL_ID || "";
|
|
27
|
+
const EDIT_TOKEN = process.env.HUNCHFUL_EDIT_TOKEN || "";
|
|
28
|
+
const CONTRIB_TOKEN = process.env.HUNCHFUL_CONTRIBUTION_TOKEN || "";
|
|
29
|
+
|
|
30
|
+
function modelIdOr(given) {
|
|
31
|
+
const id = (given || DEFAULT_MODEL || "").trim();
|
|
32
|
+
if (!id) throw new Error("No modelId given and HUNCHFUL_MODEL_ID is not set.");
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
function ok(obj) {
|
|
36
|
+
return { content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] };
|
|
37
|
+
}
|
|
38
|
+
function fail(msg) {
|
|
39
|
+
return { content: [{ type: "text", text: String(msg) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchCmp(modelId) {
|
|
43
|
+
const r = await fetch(`${BASE}/m/${encodeURIComponent(modelId)}/cmp.json`, { headers: { accept: "application/json" } });
|
|
44
|
+
if (!r.ok) throw new Error(`Couldn't read model ${modelId} (HTTP ${r.status}).`);
|
|
45
|
+
return r.json();
|
|
46
|
+
}
|
|
47
|
+
function normalizeItems(items) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
return items.map((it) => ({
|
|
50
|
+
patternId: it.patternId,
|
|
51
|
+
direction: it.direction,
|
|
52
|
+
observedAt: it.observedAt || now,
|
|
53
|
+
source: it.source || "agent:mcp",
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const itemShape = {
|
|
58
|
+
patternId: z.string().describe("One of the model's refinable axis ids (see read_model → refinablePatternIds)."),
|
|
59
|
+
direction: z.enum(["strengthens", "weakens"]).describe("Does the evidence strengthen or weaken this hunch?"),
|
|
60
|
+
observedAt: z.string().optional().describe("ISO date the evidence was observed (defaults to now)."),
|
|
61
|
+
source: z.string().optional().describe('Provenance pointer only, e.g. "agent:claude". No free text, no names.'),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const server = new McpServer({ name: "hunchful", version: "1.0.0" });
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
"read_model",
|
|
68
|
+
"Read a Hunchful collaboration model: its hunches (axis, pole, confidence), the axis ids you can refine, and how to write back. Start here.",
|
|
69
|
+
{ modelId: z.string().optional().describe("Model id. Defaults to HUNCHFUL_MODEL_ID.") },
|
|
70
|
+
async ({ modelId }) => {
|
|
71
|
+
try {
|
|
72
|
+
const id = modelIdOr(modelId);
|
|
73
|
+
const doc = await fetchCmp(id);
|
|
74
|
+
return ok({
|
|
75
|
+
modelId: id,
|
|
76
|
+
libraryVersion: doc.libraryVersion,
|
|
77
|
+
hunches: (doc.hypotheses || []).map((h) => ({ patternId: h.patternId, pole: h.pole, statement: h.statement, confidence: h.confidence })),
|
|
78
|
+
refinablePatternIds: doc.contribution?.patternIds ?? (doc.hypotheses || []).map((h) => h.patternId),
|
|
79
|
+
constraints: doc.contribution?.constraints,
|
|
80
|
+
canApplyDirectly: !!EDIT_TOKEN,
|
|
81
|
+
canPropose: !!CONTRIB_TOKEN,
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return fail(e.message || e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
server.tool(
|
|
90
|
+
"propose_refinement",
|
|
91
|
+
"Propose a refinement (provenance pointers only). It lands in the owner's queue for them to confirm — nothing changes until they do. Needs HUNCHFUL_CONTRIBUTION_TOKEN.",
|
|
92
|
+
{
|
|
93
|
+
modelId: z.string().optional(),
|
|
94
|
+
items: z.array(z.object(itemShape)).min(1).describe("Which axes to strengthen/weaken, based on what you learned."),
|
|
95
|
+
},
|
|
96
|
+
async ({ modelId, items }) => {
|
|
97
|
+
try {
|
|
98
|
+
if (!CONTRIB_TOKEN) return fail("No HUNCHFUL_CONTRIBUTION_TOKEN set. Ask the owner to generate one (their model's Edit page → \"let an agent contribute\"), or use apply_refinement with an edit token.");
|
|
99
|
+
const id = modelIdOr(modelId);
|
|
100
|
+
const doc = await fetchCmp(id).catch(() => ({}));
|
|
101
|
+
const body = { type: "hunchful.refinement", modelId: id, libraryVersion: doc.libraryVersion || "v1", contributionToken: CONTRIB_TOKEN, items: normalizeItems(items) };
|
|
102
|
+
const r = await fetch(`${BASE}/api/contribute`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
103
|
+
const j = await r.json().catch(() => ({}));
|
|
104
|
+
if (!r.ok) return fail(j.debugMessage || j.error || `HTTP ${r.status}`);
|
|
105
|
+
return ok({ status: "pending", ...j, note: "The owner confirms this in their refine page before it applies." });
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return fail(e.message || e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
server.tool(
|
|
113
|
+
"apply_refinement",
|
|
114
|
+
"Apply a refinement directly as one logged revision (append evidence, nudge confidence). Needs HUNCHFUL_EDIT_TOKEN — the owner's key. Prefer propose_refinement unless the owner asked you to write directly.",
|
|
115
|
+
{
|
|
116
|
+
modelId: z.string().optional(),
|
|
117
|
+
items: z.array(z.object(itemShape)).min(1),
|
|
118
|
+
},
|
|
119
|
+
async ({ modelId, items }) => {
|
|
120
|
+
try {
|
|
121
|
+
if (!EDIT_TOKEN) return fail("No HUNCHFUL_EDIT_TOKEN set. Direct apply needs the owner's edit key. Use propose_refinement instead (with a contribution token).");
|
|
122
|
+
const id = modelIdOr(modelId);
|
|
123
|
+
const doc = await fetchCmp(id).catch(() => ({}));
|
|
124
|
+
const body = { type: "hunchful.refinement", modelId: id, libraryVersion: doc.libraryVersion || "v1", items: normalizeItems(items), editToken: EDIT_TOKEN };
|
|
125
|
+
const r = await fetch(`${BASE}/api/apply`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
126
|
+
const j = await r.json().catch(() => ({}));
|
|
127
|
+
if (!r.ok) return fail(j.debugMessage || j.error || `HTTP ${r.status}`);
|
|
128
|
+
return ok(j);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return fail(e.message || e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const transport = new StdioServerTransport();
|
|
136
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hunchful-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Hunchful — let an AI read and refine a Hunchful collaboration model directly, instead of copy-pasting links.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hunchful-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
18
|
+
"zod": "^3.23.8"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Filip Nilsson",
|
|
22
|
+
"repository": "github:filinils/cognitive-model-protocol"
|
|
23
|
+
}
|