phaedo-mcp 0.3.1 → 0.3.3

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 CHANGED
@@ -18,12 +18,15 @@ when your phone is offline, is encrypted too.
18
18
 
19
19
  ## What you need
20
20
 
21
- 1. **The Phaedo phone app.** Your profile lives there. The app isn't open to the
22
- public yet — [phaedo.so](https://phaedo.so) has a link to hear when it is.
21
+ 1. **A profile**, from either:
22
+ - **the Phaedo phone app** — your full profile lives there. The app isn't open
23
+ to the public yet; [phaedo.so](https://phaedo.so) has a link to hear when it is.
24
+ - **your own Claude Code sessions** — no phone needed. Phaedo builds a starter
25
+ profile on this computer from how you work in Claude Code (see below).
23
26
  2. **Node.js**, free from [nodejs.org](https://nodejs.org) (the LTS button).
24
27
 
25
- Without the phone app there is no profile, and the server says so plainly rather
26
- than guessing. You can still try Phaedo with a made-up example profile.
28
+ With neither, there is no profile, and the server says so plainly rather than
29
+ guessing. You can still try Phaedo with a made-up example profile.
27
30
 
28
31
  ## Try it with the example profile (no phone needed)
29
32
 
@@ -63,6 +66,31 @@ example, delete the `env` line in Claude Desktop, or in Claude Code run
63
66
  `claude mcp remove -s user phaedo` and add it again without `-e …`. The server
64
67
  will then tell you there's no profile until you pair your phone.
65
68
 
69
+ ## Start a profile from your Claude Code sessions (no phone needed)
70
+
71
+ If you use Claude Code, Phaedo can learn from how you work there and build a
72
+ starter profile on this computer — for example, that you want short answers
73
+ without headers. It starts thin and grows as you work.
74
+
75
+ 1. **Get this folder** as in step 1 below (Code → Download ZIP, unzip, open the
76
+ `mcp` folder).
77
+ 2. **Install the Claude Code hook.** In Terminal, from the `mcp` folder:
78
+ ```bash
79
+ npm install
80
+ ```
81
+ ```bash
82
+ npm run setup:code-hook
83
+ ```
84
+ This adds Phaedo to your AI apps and turns on three things in Claude Code:
85
+ loading your profile when a session starts, checking with you before it merges
86
+ or publishes, and learning from your sessions.
87
+ 3. **Quit and reopen Claude Code**, then keep working. The first sessions build the
88
+ profile, and each later session picks up what's been learned.
89
+
90
+ What it reads stays on this computer: each session is reduced to counts (like "asked
91
+ for a shorter answer, 4 times"), and the text itself is discarded. If you pair the
92
+ phone app later, your full profile replaces the starter one and keeps what it learned.
93
+
66
94
  ## Use your own profile (with the phone app)
67
95
 
68
96
  1. **Get this folder.** On [the GitHub page](https://github.com/phaedo-labs/phaedo-mcp),
@@ -149,10 +177,14 @@ receipts-audit surfaces (the tools table above) shipped on top of these across t
149
177
  `../docs/roadmap/mcp-revocable-approval-plan.md`. Phone-gated unlock stays a
150
178
  deferred enterprise mode behind a config flag.
151
179
 
152
- **No conversation capture.** MCP never returns the conversation, so your
153
- fingerprint does not learn from MCP sessions (binding §5). (Consultation,
154
- escalation, and `phaedo_record_outcome` shape and record *decisions*, but none
155
- capture conversation text into the fingerprint.)
180
+ **Conversation capture is opt-in.** The MCP protocol never hands a server the
181
+ conversation, so the server itself learns nothing from your chats (binding §5).
182
+ Consultation, escalation and `phaedo_record_outcome` record *decisions*, not
183
+ conversation text. Capture happens only through things you turn on: the Claude
184
+ Code hooks (`npm run setup:code-hook`; see *Capture from Claude Code / Cowork*
185
+ below) and, on a Mac, the separately installed accessibility reader that feeds
186
+ `capture-a11y.js`. Both read on this computer, keep only signal names and
187
+ counts, and discard the text.
156
188
  Local **stdio** only; **standard** mode only (enhanced-privacy/Stained-Glass and
157
189
  remote HTTP/SSE are out of this cut).
158
190
 
@@ -215,9 +247,11 @@ if you prefer.)*
215
247
 
216
248
  **Phase 1a — local JSON.** With no pairing record, reads a **vault payload**
217
249
  from `PHAEDO_FINGERPRINT` (a path, or the word `sample` for the shipped
218
- `mcp/sample-fingerprint.json`), then `mcp/fingerprint.json`. With neither, there
219
- is **no profile**: every tool returns a plain-language `decryption_failed` error
220
- saying so. The sample is never served unless asked for by name — until 0.3.1 it
250
+ `mcp/sample-fingerprint.json`), then `mcp/fingerprint.json`. With neither, a
251
+ **starter profile**: the Claude Code capture state on this computer folded onto an
252
+ empty fingerprint (id `starter-local`), served only if it learned something. With
253
+ nothing captured, there is **no profile**: every tool returns a plain-language
254
+ `decryption_failed` error saying so. The sample is never served unless asked for by name — until 0.3.1 it
221
255
  was the silent fallback, so a fresh `npx phaedo-mcp` injected a made-up person's
222
256
  style without saying so. The file looks like this:
223
257
  ```json
@@ -5,10 +5,12 @@
5
5
  // Phase 1b (phone): if a pairing record is configured (PHAEDO_PAIRING or
6
6
  // mcp/pairing.json), authorize → pull /v1/profile → unwrap the §7.2 envelope
7
7
  // from the paired phone (phone-source.js). Spec-faithful; no plaintext at rest.
8
- // Phase 1a (local): otherwise read a local vault-payload JSON. With neither, there is
9
- // no profile — tools say so (NO_PROFILE_MESSAGE); the shipped sample is opt-in only.
8
+ // Phase 1a (local): otherwise read a local vault-payload JSON. With neither, a STARTER
9
+ // profile from this computer's own capture (loadStarterProfile), else no profile — tools
10
+ // say so (NO_PROFILE_MESSAGE). The shipped sample is opt-in only.
10
11
 
11
12
  import { readFile } from 'fs/promises';
13
+ import { existsSync } from 'fs';
12
14
  import { resolve, dirname } from 'path';
13
15
  import { fileURLToPath } from 'url';
14
16
  import { hasPairingRecord, loadPairingRecord, loadFingerprintFromPhone, PhoneError } from './phone-source.js';
@@ -78,6 +80,7 @@ export function foldConfigHome(vd) {
78
80
  // Resolution order for the local fixture (1a):
79
81
  // 1. PHAEDO_FINGERPRINT env var (absolute or cwd-relative path, or `sample`)
80
82
  // 2. ./fingerprint.json next to the server
83
+ // 3. a starter profile folded from this computer's capture state (see loadStarterProfile)
81
84
  // — nothing else. There is deliberately NO silent fallback to the shipped sample.
82
85
  //
83
86
  // NO PROFILE IS AN ANSWER, NOT A DEMO. Until 2026-10-20 an unpaired install with no file fell
@@ -100,8 +103,52 @@ function resolveSourcePath(baseDir) {
100
103
  export const NO_PROFILE_MESSAGE =
101
104
  'No Phaedo profile is set up on this computer, so there is nothing to load and Phaedo has ' +
102
105
  'not changed how the assistant responds. A profile comes from the Phaedo phone app, which is ' +
103
- 'not open to the public yet (https://phaedo.so to hear when it is). To try Phaedo with a made-up example profile ' +
104
- 'instead, set PHAEDO_FINGERPRINT=sample in this server\'s settings.';
106
+ 'not open to the public yet (https://phaedo.so to hear when it is). Without the phone, Phaedo ' +
107
+ 'can start one from your own Claude Code sessions on this computer once its Claude Code hook ' +
108
+ 'is installed (see the README). To try a made-up example profile instead, set ' +
109
+ 'PHAEDO_FINGERPRINT=sample in this server\'s settings.';
110
+
111
+ // The same news for the server's log (Claude Desktop's MCP log shows stderr), one line.
112
+ const NO_PROFILE_LOG =
113
+ 'no profile is set up on this computer, so Phaedo will not change how the assistant responds. ' +
114
+ 'Pair the Phaedo phone app, install the Claude Code hook to start one from your own sessions, ' +
115
+ 'or set PHAEDO_FINGERPRINT=sample to try the example.';
116
+
117
+ // STARTER PROFILE — the phone-less path.
118
+ //
119
+ // Someone without the phone app has no fingerprint to pull, but if they turned on the Claude
120
+ // Code capture hook, this computer already holds their own signal counts (capture-local.js, the
121
+ // tier-3 owner). Folding those onto an EMPTY fingerprint gives a profile made only of what they
122
+ // did — never the sample's invented traits, which is what this branch fell back to until 0.3.0.
123
+ // It is thin by construction (persona_strength 0; the projection's confidence section says
124
+ // "treat all signals as preliminary"), and that is the honest label for it.
125
+ //
126
+ // Served only when the fold learned something: zero signals is still "no profile". The id is a
127
+ // constant so receipts and the agreement metric stay on one subject across loads; pairing a
128
+ // phone later replaces it (the phone inherits the cumulative counts — capture-local.js §5).
129
+ export const STARTER_FINGERPRINT_ID = 'starter-local';
130
+
131
+ async function loadStarterProfile(now = new Date().toISOString()) {
132
+ if (!await isLocalCaptureOwner(false)) return null;
133
+ const base = { phaedo_fingerprint: { fingerprint_id: STARTER_FINGERPRINT_ID, updated_at: now, layers: {} } };
134
+ const vd = await foldLocalCapture(base, { now });
135
+ const layers = (vd && vd.phaedo_fingerprint && vd.phaedo_fingerprint.layers) || {};
136
+ if (!Object.values(layers).some((l) => Array.isArray(l?.signals) && l.signals.length)) return null;
137
+ return markStarter(foldConfigHome(normalizeIdentity(vd)));
138
+ }
139
+
140
+ /** One line for the server log describing where the profile will come from, or null when there
141
+ * is nothing a person needs told. Never touches the phone: a pull at startup would put an
142
+ * "authorize" card on it with nobody asking. */
143
+ export async function profileStatusLine(baseDir) {
144
+ const env = process.env.PHAEDO_FINGERPRINT;
145
+ if (env === SAMPLE_KEYWORD) return 'using the made-up example profile (PHAEDO_FINGERPRINT=sample).';
146
+ if (env) return null;
147
+ if (hasPairingRecord(baseDir)) return null;
148
+ if (existsSync(resolve(baseDir, 'fingerprint.json'))) return null;
149
+ if (await loadStarterProfile()) return 'no phone paired; using a starter profile built from your own sessions on this computer.';
150
+ return NO_PROFILE_LOG;
151
+ }
105
152
 
106
153
  // Returns the vault payload:
107
154
  // { phaedo_fingerprint, phaedo_behavioral_signals, phaedo_linguistic_profile,
@@ -131,6 +178,18 @@ function stamp(vd, cached) {
131
178
  return vd;
132
179
  }
133
180
 
181
+ // A starter profile is read fresh every load (live), but it did not come from a phone —
182
+ // `pull.js` must not say it did.
183
+ function markStarter(vd) {
184
+ try {
185
+ Object.defineProperty(vd, SOURCE, {
186
+ value: { live: true, cachedAt: null, ageMs: 0, starter: true },
187
+ enumerable: false, configurable: true, writable: true,
188
+ });
189
+ } catch { /* frozen object — provenance is best-effort, never fatal */ }
190
+ return vd;
191
+ }
192
+
134
193
  /** Provenance of a loaded fingerprint. Absent marker → a live read. */
135
194
  export function sourceOf(vd) {
136
195
  const s = vd && vd[SOURCE];
@@ -179,6 +238,8 @@ export async function loadFingerprint(baseDir) {
179
238
  // Only the default location may be absent quietly. A path the user NAMED that doesn't
180
239
  // read is their mistake to see, not "no profile".
181
240
  if (!process.env.PHAEDO_FINGERPRINT && err?.code === 'ENOENT') {
241
+ const starter = await loadStarterProfile();
242
+ if (starter) return starter;
182
243
  throw new PhaedoError('decryption_failed', NO_PROFILE_MESSAGE);
183
244
  }
184
245
  throw new PhaedoError('decryption_failed', `Could not read the Phaedo profile at ${path} (${err?.code || err?.message || err}).`);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "phaedo-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
- "description": "Phaedo MCP server \u2014 inject your cognitive fingerprint into any MCP client, and consult your decision pattern at agent decision points (docs/protocol/mcp-binding.md).",
7
+ "description": "Lets Claude, Cursor and other MCP apps answer in your style, check with you before acting, send decisions to your phone to approve, and ask you to sign purchase approvals with Face ID. Needs the Phaedo phone app; set PHAEDO_FINGERPRINT=sample to try it.",
8
8
  "mcpName": "io.github.phaedo-labs/phaedo-mcp",
9
9
  "homepage": "https://phaedo.so",
10
10
  "repository": {
@@ -14,11 +14,20 @@
14
14
  },
15
15
  "keywords": [
16
16
  "mcp",
17
+ "mcp-server",
17
18
  "model-context-protocol",
19
+ "claude",
20
+ "claude-code",
21
+ "cursor",
18
22
  "personalization",
19
- "fingerprint",
20
- "consultation",
21
- "agent"
23
+ "human-in-the-loop",
24
+ "approval",
25
+ "guardrails",
26
+ "agent",
27
+ "ai-agents",
28
+ "ap2",
29
+ "agent-payments",
30
+ "anamnesis"
22
31
  ],
23
32
  "bin": {
24
33
  "phaedo-mcp": "server.js"
package/pull.js CHANGED
@@ -21,6 +21,13 @@ loadFingerprint(BASE_DIR)
21
21
  // profile went unnoticed while the agent answered authorization questions from it. The
22
22
  // whole point of this command is to verify the pairing works, so it has to distinguish
23
23
  // "your phone answered" from "your phone didn't, here's an old copy".
24
+ if (src.starter) {
25
+ process.stdout.write(
26
+ `✓ No phone paired — serving a starter profile built from your own sessions on this computer, ${layers} layer(s).\n` +
27
+ ` Pair the Phaedo phone app (npm run pair) to replace it with your full profile.\n`
28
+ );
29
+ process.exit(0);
30
+ }
24
31
  if (src.live) {
25
32
  process.stdout.write(
26
33
  `✓ Read your profile from the phone — fingerprint ${fp.fingerprint_id || '(no id)'}, ${layers} layer(s).\n`
package/server.js CHANGED
@@ -16,7 +16,7 @@ import { z } from 'zod';
16
16
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
17
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
18
 
19
- import { loadFingerprint, sourceOf } from './fingerprint-source.js';
19
+ import { loadFingerprint, sourceOf, profileStatusLine } from './fingerprint-source.js';
20
20
  import { hasPairingRecord, loadPairingRecord } from './phone-source.js';
21
21
  import { depositDelegationSuggestions } from './delegation-sync.js';
22
22
  import { depositReceiptsDigest, drainAndApplyMarks } from './receipts-sync.js';
@@ -190,7 +190,18 @@ if (existsSync(ESCALATION_POLICY_PATH)) {
190
190
  // been the package's for two months. One source of truth; test-package-closure.mjs
191
191
  // asserts the two agree.
192
192
  const PKG_VERSION = JSON.parse(readFileSync(join(BASE_DIR, 'package.json'), 'utf8')).version;
193
- const server = new McpServer({ name: 'phaedo', version: PKG_VERSION });
193
+ // The one place a client is told how the tools fit together. Without it an app saw eight
194
+ // tool descriptions and nothing saying "load the profile first" or "consult before you ship" —
195
+ // so whether Phaedo was used at all depended on the model guessing. Short on purpose: clients
196
+ // put this in the model's context on every turn.
197
+ const SERVER_INSTRUCTIONS = [
198
+ 'Phaedo holds the user\'s profile: how they like answers written, and how they want decisions handled when an AI acts for them.',
199
+ '- At the start of a session, call phaedo_request_injection once and treat the returned projection as background on the user\'s style.',
200
+ '- Before a consequential action for the user (merging, deploying, publishing, sending a message, spending money, deleting, changing an account or setting), call phaedo_consult and follow its signal. If you are acting on your own and the decision should be the user\'s, use phaedo_escalate and do not proceed until it resolves.',
201
+ '- When the user reacts to an action you consulted on, call phaedo_record_outcome with its receipt_id: record what they chose, never what you inferred.',
202
+ '- If a tool says no Phaedo profile is set up, tell the user once, plainly, and carry on normally. Do not retry.',
203
+ ].join('\n');
204
+ const server = new McpServer({ name: 'phaedo', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS });
194
205
 
195
206
  // ── Tool: phaedo_request_injection (binding §3.1) ─────────────────────────────
196
207
  server.registerTool(
@@ -252,8 +263,8 @@ const ACTION_BINDING_SCHEMA = z
252
263
  .optional()
253
264
  .describe(
254
265
  'Binds this consultation to the concrete call it describes. The action_descriptor is your ACCOUNT of the action; ' +
255
- 'this is the action. Recorded on the audit receipt so a later mismatch is provable, shown to the subject when an ' +
256
- 'escalation reaches their phone, and REQUIRED by any standing rule the subject marked require_binding before it ' +
266
+ 'this is the action. Recorded on the audit receipt so a later mismatch is provable, shown to the user when an ' +
267
+ 'escalation reaches their phone, and REQUIRED by any standing rule the user marked require_binding before it ' +
257
268
  'will auto-proceed.',
258
269
  );
259
270
 
@@ -348,7 +359,7 @@ server.registerTool(
348
359
  title: 'Check standing authorizations for an action',
349
360
  description:
350
361
  "Deterministic pre-flight: check whether one of the user's standing authorizations " +
351
- '(§10.5) already decides this action — escalate, decline, or pre-approve — without running ' +
362
+ 'already decides this action — escalate, decline, or pre-approve — without running ' +
352
363
  'the full consultation. Returns authorization_matched plus the authorized signal and the ' +
353
364
  'rule that fired. If no rule matches, fall back to phaedo_consult or your own default.',
354
365
  inputSchema: {
@@ -666,10 +677,10 @@ server.registerTool(
666
677
  {
667
678
  title: 'Ask the subject to sign a payment authorization (AP2 Intent Mandate)',
668
679
  description:
669
- 'Request the subject\'s cryptographic signature on an AP2 Intent Mandate built from their ' +
680
+ 'Request the user\'s cryptographic signature on an AP2 Intent Mandate built from their ' +
670
681
  'standing authorizations — a credential a merchant or payments network can verify. Returns a ' +
671
682
  'request_id and the unsigned mandate immediately; then poll phaedo_sign_status with BOTH, and ' +
672
- 'DO NOT treat the mandate as authorized until it returns signed. The subject signs on their ' +
683
+ 'DO NOT treat the mandate as authorized until it returns signed. The user signs on their ' +
673
684
  'phone (Face ID, key in the Secure Enclave); they may also decline. Use only when you need a ' +
674
685
  'verifiable payment credential — not for ordinary consultations.',
675
686
  inputSchema: {
@@ -719,7 +730,7 @@ server.registerTool(
719
730
  mandate,
720
731
  message: 'Sent to the subject to sign. Poll phaedo_sign_status with this request_id AND this ' +
721
732
  'mandate; do not treat it as authorized until it returns signed. If it expires or the ' +
722
- 'subject declines, the mandate stays an unsigned proposal.',
733
+ 'user declines, the mandate stays an unsigned proposal.',
723
734
  }
724
735
  : {
725
736
  request_type: 'sign_mandate', status: 'unreachable', reason: dep.reason,
@@ -745,9 +756,9 @@ server.registerTool(
745
756
  title: 'Check whether the subject has signed the mandate',
746
757
  description:
747
758
  'Poll a request from phaedo_sign_mandate. Pass the request_id AND the mandate that call returned. ' +
748
- 'Returns signed + the mandate carrying the verified signature once the subject signs, refused if ' +
759
+ 'Returns signed + the mandate carrying the verified signature once the user signs, refused if ' +
749
760
  'they declined, pending while you should keep waiting, or expired once the window lapsed. A signed ' +
750
- 'result is verified against the subject\'s published signing key before it is returned.',
761
+ 'result is verified against the user\'s published signing key before it is returned.',
751
762
  inputSchema: {
752
763
  request_id: z.string(),
753
764
  mandate: z.record(z.any()),
@@ -881,6 +892,12 @@ async function main() {
881
892
  await server.connect(transport);
882
893
  // stdio transport owns stdout for protocol framing — never console.log here.
883
894
  process.stderr.write('[phaedo-mcp] ready on stdio\n');
895
+ // Where the profile will come from, in one line, after the handshake so startup never waits
896
+ // on it. stderr is what Claude Desktop's MCP log shows — the one place a person debugging
897
+ // "why isn't Phaedo doing anything" will look. Best-effort: a failure here says nothing.
898
+ profileStatusLine(BASE_DIR)
899
+ .then((line) => { if (line) process.stderr.write(`[phaedo-mcp] ${line}\n`); })
900
+ .catch(() => {});
884
901
  }
885
902
 
886
903
  main().catch((err) => {
package/server.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.phaedo-labs/phaedo-mcp",
4
- "description": "Carry your Phaedo cognitive fingerprint into any MCP client: inject, consult, escalate, sign.",
5
- "version": "0.3.1",
4
+ "description": "Make AI apps answer your way, check with you before acting, and get approvals on your phone.",
5
+ "version": "0.3.3",
6
6
  "repository": {
7
7
  "url": "https://github.com/phaedo-labs/phaedo-mcp",
8
8
  "source": "github",
@@ -13,7 +13,7 @@
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "phaedo-mcp",
16
- "version": "0.3.1",
16
+ "version": "0.3.3",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }