@sbains2/lifeos 0.1.3 → 0.2.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/src/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { join, relative } from 'node:path';
3
+ import { homedir } from 'node:os';
3
4
  import { confirm } from '@inquirer/prompts';
4
- import { loadPersonaTemplate } from './templates-loader.js';
5
+ import { loadPersonaTemplate, loadRegistry } from './templates-loader.js';
5
6
  import { askPersona } from './wizard/persona.js';
6
7
  import { askProfile } from './wizard/profile.js';
7
8
  import { askFoci } from './wizard/foci.js';
@@ -10,6 +11,14 @@ import { askCouncil } from './wizard/council.js';
10
11
  import { askMcps } from './wizard/mcps.js';
11
12
  import { validateAndGenerate } from './wizard/generate.js';
12
13
  import { cleanupQuadrantMcpRefs } from './cleanup.js';
14
+ import {
15
+ writeProjectMcpJson,
16
+ detectClaudeDesktop,
17
+ mergeIntoClaudeDesktop,
18
+ generateMcpStatus,
19
+ groupMcpStatus,
20
+ CLAUDE_DESKTOP_CONFIG_PATH,
21
+ } from './mcp-config.js';
13
22
  import { startTimer, elapsed } from './utils/timer.js';
14
23
  import { c } from './utils/colors.js';
15
24
 
@@ -17,7 +26,7 @@ export async function run(targetDir, options = {}) {
17
26
  const { minimal = false } = options;
18
27
 
19
28
  console.log('');
20
- console.log(c.bold('lifeos') + c.dim(' v0.1.3 — scaffold a personal LifeOS'));
29
+ console.log(c.bold('lifeos') + c.dim(' v0.2.0 — scaffold a personal LifeOS'));
21
30
  console.log(c.dim(`Target directory: ${targetDir}`));
22
31
  if (minimal) {
23
32
  console.log(c.dim('Mode: --minimal (speed-optimized; using template defaults for quadrants, council, MCPs)'));
@@ -74,8 +83,6 @@ export async function run(targetDir, options = {}) {
74
83
 
75
84
  // Auto-cleanup: if the user unchecked any MCP at Step 6 that a quadrant
76
85
  // still references, remove the dangling reference so validation passes.
77
- // (Reported as a bug 2026-05-01 — friend tester unchecked zotero and the
78
- // research quadrant still referenced it, blocking the entire install.)
79
86
  const { cleaned: cleanedQuadrants, removed: cleanupReport } = cleanupQuadrantMcpRefs(quadrants, mcps);
80
87
  if (cleanupReport.length > 0) {
81
88
  console.log('');
@@ -85,6 +92,32 @@ export async function run(targetDir, options = {}) {
85
92
  }
86
93
  }
87
94
 
95
+ // Risk-advisory check: if any wired MCP is `use-at-your-own-risk`, surface the
96
+ // warning and ask whether to include it in auto-config (.mcp.json + Claude Desktop).
97
+ // The MCP stays in lifeos.config.json regardless — this only controls auto-install.
98
+ const registry = (() => {
99
+ try { return loadRegistry(); } catch { return { servers: [] }; }
100
+ })();
101
+ const registryById = new Map(registry.servers.map((s) => [s.id, s]));
102
+ const riskyPicked = mcps.filter((m) => registryById.get(m.id)?.status === 'use-at-your-own-risk');
103
+ let includeRisky = false;
104
+ if (riskyPicked.length > 0 && !minimal) {
105
+ console.log('');
106
+ console.log(c.warn('Some MCPs you picked are marked use-at-your-own-risk:'));
107
+ for (const m of riskyPicked) {
108
+ const entry = registryById.get(m.id);
109
+ console.log(` ${c.warn('!')} ${c.bold(m.id)}: ${entry.risk_advisory ?? '(see registry)'}`);
110
+ }
111
+ includeRisky = await confirm({
112
+ message: 'Include these in auto-config? (No = MCP stays in lifeos.config.json but skipped from .mcp.json — install manually)',
113
+ default: false,
114
+ });
115
+ } else if (riskyPicked.length > 0 && minimal) {
116
+ // --minimal: include by default to avoid blocking, but log
117
+ includeRisky = true;
118
+ console.log(c.dim(` (--minimal: including ${riskyPicked.length} use-at-your-own-risk MCP(s) without prompt — see lifeos.config.json risk_advisory)`));
119
+ }
120
+
88
121
  // Assemble final config
89
122
  const config = {
90
123
  $schema: template.$schema,
@@ -106,7 +139,6 @@ export async function run(targetDir, options = {}) {
106
139
  ...template.brain,
107
140
  graphify: {
108
141
  ...template.brain.graphify,
109
- // Reduce graphify paths to active quadrants only
110
142
  paths: cleanedQuadrants.filter((q) => q.active).map((q) => q.path),
111
143
  },
112
144
  },
@@ -115,17 +147,114 @@ export async function run(targetDir, options = {}) {
115
147
 
116
148
  // Step 7 — validate + scaffold
117
149
  const result = await validateAndGenerate(config, { targetDir, minimal });
150
+ if (!result.ok) {
151
+ console.log(c.dim(`Aborted after ${elapsed(start)}.`));
152
+ return;
153
+ }
118
154
 
155
+ // Post-scaffold: auto-config MCPs
156
+ // (1) Always write project-local .mcp.json — low-risk, scoped to the user's repo
119
157
  console.log('');
120
- if (result.ok) {
121
- console.log(c.bold(c.ok('Done')) + ` in ${elapsed(start)}.`);
122
- console.log('');
123
- console.log(c.bold('Next:'));
124
- console.log(` ${c.arrow} Wire your MCP servers (see .lifeos/INSTALL_MCPS.md)`);
125
- console.log(` ${c.arrow} Open this folder in Claude Code and try convening the council on a real artifact`);
126
- console.log(` ${c.arrow} Edit lifeos.config.json to tune council priorities, add quadrants, or change foci`);
127
- console.log('');
158
+ console.log(c.bold('Wiring MCP servers...'));
159
+ const mcpJsonResult = writeProjectMcpJson(targetDir, mcps, registry, { includeRisky });
160
+ console.log(` ${c.check} wrote ${relative(homedir(), mcpJsonResult.path).replace(/^/, '~/')} (${mcpJsonResult.added} entries)`);
161
+ if (mcpJsonResult.skipped.length > 0) {
162
+ for (const s of mcpJsonResult.skipped) {
163
+ console.log(c.dim(` ${c.dot} skipped ${s.id}: ${s.reason}`));
164
+ }
165
+ }
166
+
167
+ // (2) Detect Claude Desktop config; offer to merge
168
+ let desktopMergeResult = null;
169
+ if (detectClaudeDesktop()) {
170
+ if (!minimal) {
171
+ console.log('');
172
+ console.log(c.dim(`Detected Claude Desktop config at ${CLAUDE_DESKTOP_CONFIG_PATH.replace(homedir(), '~')}`));
173
+ const doMerge = await confirm({
174
+ message: 'Also merge wired MCPs into Claude Desktop config? (backup taken first; existing entries by name preserved)',
175
+ default: true,
176
+ });
177
+ if (doMerge) {
178
+ try {
179
+ desktopMergeResult = mergeIntoClaudeDesktop(mcps, registry, { includeRisky });
180
+ console.log(` ${c.check} merged into Claude Desktop config`);
181
+ console.log(c.dim(` backup: ${desktopMergeResult.backupPath.replace(homedir(), '~')}`));
182
+ if (desktopMergeResult.addedIds.length > 0) {
183
+ console.log(c.dim(` added: ${desktopMergeResult.addedIds.join(', ')}`));
184
+ }
185
+ if (desktopMergeResult.skippedIds.length > 0) {
186
+ console.log(c.dim(` skipped (already in your config): ${desktopMergeResult.skippedIds.join(', ')}`));
187
+ }
188
+ } catch (err) {
189
+ console.log(c.err(` ${c.cross} merge failed: ${err.message}`));
190
+ console.log(c.dim(` .mcp.json was still written — Claude Code / Cursor will pick that up`));
191
+ }
192
+ } else {
193
+ console.log(c.dim(` ${c.dot} skipped Claude Desktop merge — only .mcp.json written`));
194
+ }
195
+ } else {
196
+ console.log(c.dim(` ${c.dot} (--minimal: skipping Claude Desktop merge prompt — only .mcp.json written)`));
197
+ }
128
198
  } else {
129
- console.log(c.dim(`Aborted after ${elapsed(start)}.`));
199
+ console.log(c.dim(` ${c.dot} Claude Desktop config not found — only .mcp.json written`));
130
200
  }
201
+
202
+ // (3) Per-MCP status report
203
+ const mcpStatus = generateMcpStatus(mcps, registry);
204
+ const groups = groupMcpStatus(mcpStatus);
205
+ console.log('');
206
+ console.log(c.bold(`MCP setup status (${mcps.length} wired):`));
207
+ if (groups.ready.length > 0) {
208
+ console.log(` ${c.ok('✓ ready')} (${groups.ready.length}): ${groups.ready.map((r) => r.id).join(', ')}`);
209
+ console.log(c.dim(` these work as soon as Claude restarts`));
210
+ }
211
+ if (groups.needs_oauth.length > 0) {
212
+ console.log(` ${c.warn('⚠ OAuth on first use')} (${groups.needs_oauth.length}):`);
213
+ for (const r of groups.needs_oauth) {
214
+ console.log(` ${c.bold(r.id)}`);
215
+ for (const line of r.lines) console.log(c.dim(` ${line}`));
216
+ }
217
+ }
218
+ if (groups.needs_env.length > 0) {
219
+ console.log(` ${c.warn('⚠ needs env var')} (${groups.needs_env.length}):`);
220
+ for (const r of groups.needs_env) {
221
+ console.log(` ${c.bold(r.id)}`);
222
+ for (const line of r.lines) console.log(c.dim(` ${line}`));
223
+ }
224
+ }
225
+ if (groups.manual.length > 0) {
226
+ console.log(` ${c.warn('⚠ manual install required')} (${groups.manual.length}):`);
227
+ for (const r of groups.manual) {
228
+ console.log(` ${c.bold(r.id)}`);
229
+ for (const line of r.lines) console.log(c.dim(` ${line}`));
230
+ }
231
+ }
232
+ if (groups.risk.length > 0) {
233
+ console.log(` ${c.err('⚠ use-at-your-own-risk')} (${groups.risk.length}):`);
234
+ for (const r of groups.risk) {
235
+ console.log(` ${c.bold(r.id)}`);
236
+ for (const line of r.lines) console.log(c.dim(` ${line}`));
237
+ }
238
+ }
239
+ if (groups.unknown.length > 0) {
240
+ console.log(` ${c.dim('? unknown (not in registry)')} (${groups.unknown.length}): ${groups.unknown.map((r) => r.id).join(', ')}`);
241
+ }
242
+
243
+ // Final summary
244
+ console.log('');
245
+ console.log(c.bold(c.ok('Done')) + ` in ${elapsed(start)}.`);
246
+ console.log('');
247
+ console.log(c.bold('Next:'));
248
+ if (groups.needs_env.length > 0) {
249
+ console.log(` ${c.arrow} Set the env vars listed above (in your shell rc — ~/.zshrc or ~/.bashrc), then reload your shell`);
250
+ }
251
+ if (desktopMergeResult || groups.ready.length > 0 || groups.needs_oauth.length > 0) {
252
+ console.log(` ${c.arrow} Restart Claude Desktop (Cmd+Q, then reopen) so the new MCPs load`);
253
+ }
254
+ console.log(` ${c.arrow} Open ${targetDir} in Claude Code and try convening the council on a real artifact`);
255
+ console.log(` ${c.arrow} Edit lifeos.config.json to tune council priorities, add quadrants, or change foci`);
256
+ if (groups.manual.length > 0 || groups.risk.length > 0) {
257
+ console.log(` ${c.arrow} For manual / risky MCPs: see .lifeos/INSTALL_MCPS.md for the full hint per server`);
258
+ }
259
+ console.log('');
131
260
  }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * MCP auto-configuration: write .mcp.json into the LifeOS scaffold and
3
+ * optionally merge into the user's Claude Desktop config.
4
+ *
5
+ * Trust posture:
6
+ * - The local .mcp.json is always written (project-scoped, low-risk).
7
+ * - The Claude Desktop config is only touched after explicit user confirmation,
8
+ * always backed up first, and we MERGE — never overwrite existing entries by name.
9
+ * - Auth tokens are referenced by env-var name only. The CLI never reads token
10
+ * values from anywhere or writes them to the config.
11
+ */
12
+
13
+ import { writeFileSync, readFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs';
14
+ import { join, dirname } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+
17
+ export const CLAUDE_DESKTOP_CONFIG_PATH = join(
18
+ homedir(),
19
+ 'Library',
20
+ 'Application Support',
21
+ 'Claude',
22
+ 'claude_desktop_config.json'
23
+ );
24
+
25
+ /**
26
+ * Build the `mcpServers` block for a list of wired MCPs.
27
+ * - Looks each MCP up in the registry.
28
+ * - Skips entries with `runtime: "manual"` or no `install_command`.
29
+ * - Skips entries with status `use-at-your-own-risk` UNLESS explicitly opted in
30
+ * (caller passes { includeRisky: true }) — these need explicit user confirmation.
31
+ *
32
+ * Returns { servers, skipped: [{id, reason}] }.
33
+ */
34
+ export function buildMcpServersBlock(wiredMcps, registry, options = {}) {
35
+ const { includeRisky = false } = options;
36
+ const byId = new Map((registry?.servers ?? []).map((s) => [s.id, s]));
37
+ const servers = {};
38
+ const skipped = [];
39
+
40
+ for (const m of wiredMcps ?? []) {
41
+ const entry = byId.get(m.id);
42
+ if (!entry) {
43
+ skipped.push({ id: m.id, reason: 'not in registry' });
44
+ continue;
45
+ }
46
+ if (!entry.install_command || entry.runtime === 'manual') {
47
+ skipped.push({ id: m.id, reason: 'manual install (see install_hint)' });
48
+ continue;
49
+ }
50
+ if (entry.status === 'use-at-your-own-risk' && !includeRisky) {
51
+ skipped.push({ id: m.id, reason: 'use-at-your-own-risk — opt in explicitly' });
52
+ continue;
53
+ }
54
+
55
+ const [command, ...args] = entry.install_command;
56
+ const def = { command, args };
57
+ if (entry.auth?.env_var) {
58
+ // Reference by name only — value comes from user's shell env at runtime
59
+ def.env = { [entry.auth.env_var]: `\${${entry.auth.env_var}}` };
60
+ }
61
+ servers[m.id] = def;
62
+ }
63
+
64
+ return { servers, skipped };
65
+ }
66
+
67
+ /**
68
+ * Write `<targetDir>/.mcp.json` with the wired MCPs.
69
+ * If the file already exists, MERGE — preserve any user-added entries.
70
+ * Returns { path, added: number, skipped: [] }.
71
+ */
72
+ export function writeProjectMcpJson(targetDir, wiredMcps, registry, options = {}) {
73
+ const { servers, skipped } = buildMcpServersBlock(wiredMcps, registry, options);
74
+ const path = join(targetDir, '.mcp.json');
75
+
76
+ let existing = { mcpServers: {} };
77
+ if (existsSync(path)) {
78
+ try {
79
+ existing = JSON.parse(readFileSync(path, 'utf8'));
80
+ } catch {
81
+ // Malformed existing file — skip merge, write fresh (preserve via backup below)
82
+ const backupPath = `${path}.malformed-backup`;
83
+ copyFileSync(path, backupPath);
84
+ existing = { mcpServers: {} };
85
+ }
86
+ }
87
+
88
+ const existingServers = existing.mcpServers ?? {};
89
+ const merged = {
90
+ ...existing,
91
+ mcpServers: {
92
+ ...existingServers,
93
+ // New entries take precedence over old ones with same id only if user explicitly
94
+ // chose them in the wizard. We assume that's intentional (re-run wizard = update).
95
+ ...servers,
96
+ },
97
+ };
98
+
99
+ writeFileSync(path, JSON.stringify(merged, null, 2) + '\n', 'utf8');
100
+ return { path, added: Object.keys(servers).length, skipped };
101
+ }
102
+
103
+ /**
104
+ * Returns true if Claude Desktop is installed (config file exists).
105
+ * Read-only — does not modify anything.
106
+ */
107
+ export function detectClaudeDesktop(configPath = CLAUDE_DESKTOP_CONFIG_PATH) {
108
+ return existsSync(configPath);
109
+ }
110
+
111
+ /**
112
+ * Backup + merge our MCPs into the Claude Desktop config.
113
+ * Always writes a backup at `<configPath>.lifeos-backup.<timestamp>` before any change.
114
+ * NEVER overwrites existing mcpServers entries by name — preserves user customization.
115
+ *
116
+ * Returns { backupPath, addedIds: [], skippedIds: [] (because already present in user config) }.
117
+ * Throws if config file doesn't exist or can't be parsed.
118
+ */
119
+ export function mergeIntoClaudeDesktop(wiredMcps, registry, options = {}) {
120
+ const { configPath = CLAUDE_DESKTOP_CONFIG_PATH } = options;
121
+ if (!existsSync(configPath)) {
122
+ throw new Error(`Claude Desktop config not found at ${configPath}`);
123
+ }
124
+
125
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
126
+ const backupPath = `${configPath}.lifeos-backup.${timestamp}`;
127
+ copyFileSync(configPath, backupPath);
128
+
129
+ let existing;
130
+ try {
131
+ existing = JSON.parse(readFileSync(configPath, 'utf8'));
132
+ } catch (err) {
133
+ throw new Error(
134
+ `Could not parse Claude Desktop config (${err.message}). ` +
135
+ `Original preserved at ${backupPath} — fix the JSON manually before retrying.`
136
+ );
137
+ }
138
+
139
+ const { servers } = buildMcpServersBlock(wiredMcps, registry, options);
140
+ const existingServers = existing.mcpServers ?? {};
141
+ const existingIds = new Set(Object.keys(existingServers));
142
+
143
+ const addedIds = [];
144
+ const skippedIds = [];
145
+ const newServers = {};
146
+ for (const [id, def] of Object.entries(servers)) {
147
+ if (existingIds.has(id)) {
148
+ skippedIds.push(id);
149
+ } else {
150
+ newServers[id] = def;
151
+ addedIds.push(id);
152
+ }
153
+ }
154
+
155
+ const merged = {
156
+ ...existing,
157
+ mcpServers: { ...existingServers, ...newServers },
158
+ };
159
+
160
+ writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
161
+ return { backupPath, addedIds, skippedIds };
162
+ }
163
+
164
+ /**
165
+ * Generate a per-MCP status report. Returns an array of objects suitable for
166
+ * display in the wizard's final summary. Each entry: { id, status, lines: [] }
167
+ *
168
+ * Statuses:
169
+ * - "ready" — install_command + no auth needed
170
+ * - "needs_env" — install_command exists; user must set an env var
171
+ * - "needs_oauth"— install_command exists; user must complete OAuth on first run
172
+ * - "manual" — no install_command (manual install required)
173
+ * - "risk" — use-at-your-own-risk; surface advisory prominently
174
+ * - "unknown" — not in registry (only happens if user added an ad-hoc MCP)
175
+ */
176
+ export function generateMcpStatus(wiredMcps, registry) {
177
+ const byId = new Map((registry?.servers ?? []).map((s) => [s.id, s]));
178
+ const report = [];
179
+
180
+ for (const m of wiredMcps ?? []) {
181
+ const entry = byId.get(m.id);
182
+ if (!entry) {
183
+ report.push({ id: m.id, status: 'unknown', lines: ['not in registry'] });
184
+ continue;
185
+ }
186
+
187
+ const lines = [];
188
+ let status;
189
+
190
+ if (entry.status === 'use-at-your-own-risk') {
191
+ status = 'risk';
192
+ lines.push(`⚠ ${entry.risk_advisory?.split('.')[0] ?? 'use at your own risk'}`);
193
+ if (entry.repo_url) lines.push(` repo: ${entry.repo_url}`);
194
+ } else if (!entry.install_command || entry.runtime === 'manual') {
195
+ status = 'manual';
196
+ lines.push(`manual install required`);
197
+ if (entry.repo_url) lines.push(` repo: ${entry.repo_url}`);
198
+ if (entry.install_hint) lines.push(` ${entry.install_hint.split('.')[0]}`);
199
+ } else if (entry.auth?.method === 'oauth') {
200
+ status = 'needs_oauth';
201
+ lines.push(`OAuth on first use (Claude will prompt)`);
202
+ if (entry.auth.help_url) lines.push(` setup help: ${entry.auth.help_url}`);
203
+ if (entry.auth.scope_hint) lines.push(` ${entry.auth.scope_hint.split('.')[0]}`);
204
+ } else if (entry.auth?.method === 'api_key' && entry.auth?.env_var) {
205
+ status = 'needs_env';
206
+ lines.push(`needs ${entry.auth.env_var}`);
207
+ if (entry.auth.help_url) lines.push(` get one: ${entry.auth.help_url}`);
208
+ if (entry.auth.scope_hint) lines.push(` scope: ${entry.auth.scope_hint.split('.')[0]}`);
209
+ } else {
210
+ status = 'ready';
211
+ lines.push(`ready (${entry.runtime})`);
212
+ }
213
+
214
+ report.push({ id: m.id, status, lines });
215
+ }
216
+
217
+ return report;
218
+ }
219
+
220
+ /**
221
+ * Group a status report by status for printing.
222
+ * Returns { ready: [], needs_env: [], needs_oauth: [], manual: [], risk: [], unknown: [] }.
223
+ */
224
+ export function groupMcpStatus(report) {
225
+ const groups = { ready: [], needs_oauth: [], needs_env: [], manual: [], risk: [], unknown: [] };
226
+ for (const entry of report) {
227
+ if (groups[entry.status]) groups[entry.status].push(entry);
228
+ else groups.unknown.push(entry);
229
+ }
230
+ return groups;
231
+ }
@@ -91,13 +91,44 @@ Roster of council voices. Each member is defined once globally and referenced by
91
91
 
92
92
  ### `mcp_servers`
93
93
 
94
- Wired MCP servers — id-only references, with optional per-server overrides. The CLI's onboarding wizard suggests these from `templates/mcps/registry.json` based on the user's app stack.
94
+ Wired MCP servers — id-only references in the user's `lifeos.config.json`, with optional per-server overrides. The CLI's onboarding wizard suggests these from `templates/mcps/registry.json` based on the user's app stack.
95
+
96
+ In the user's config:
95
97
 
96
98
  | Field | Type | Required | Notes |
97
99
  |---|---|---|---|
98
100
  | `id` | string | yes | Slug; must match a registry entry |
99
101
  | `config_overrides` | object | no | Per-server config (e.g., scopes, allowed folders, send_permission) |
100
102
 
103
+ In the registry (`templates/mcps/registry.json`), each entry carries the metadata needed to install + auth:
104
+
105
+ | Field | Type | Required | Notes |
106
+ |---|---|---|---|
107
+ | `id` | string | yes | Slug |
108
+ | `name` | string | yes | Display name |
109
+ | `category` | string | yes | Grouping (productivity, communication, engineering, etc.) |
110
+ | `description` | string | yes | One-sentence description |
111
+ | `status` | enum | yes | `reference` / `vendor-published` / `community` / `beta-unverified` / `use-at-your-own-risk` |
112
+ | `repo_url` | string | no | Verified GitHub URL (null if beta-unverified) |
113
+ | `runtime` | enum | yes | `npx` / `uvx` / `pip-install` / `git+pip` / `docker` / `remote` / `manual` |
114
+ | `install_command` | string[] | no | Argv-style command to install/run the MCP. `null` if `manual` runtime |
115
+ | `auth.method` | enum | yes | `oauth` / `api_key` / `none` |
116
+ | `auth.env_var` | string | no | Name of the env var holding the credential |
117
+ | `auth.help_url` | string | no | Where the user obtains credentials (e.g., `https://github.com/settings/tokens`) |
118
+ | `auth.scope_hint` | string | no | Recommended scopes/permissions |
119
+ | `risk_advisory` | string | no | Free-text warning surfaced for `use-at-your-own-risk` entries |
120
+ | `useful_for_personas` | string[] | yes | Which persona templates suggest this MCP |
121
+ | `last_verified` | string | yes | YYYY-MM-DD when registry entry was last verified against upstream |
122
+ | `install_hint` | string | yes | Free-text fallback / additional context for the install path |
123
+
124
+ #### Status enum reference
125
+
126
+ - `reference` — Maintained by the MCP steering group. Most stable.
127
+ - `vendor-published` — Published by the vendor whose product it integrates with (e.g. Stripe, Notion, Microsoft for Playwright). Generally vendor-supported.
128
+ - `community` — Community-built, actively maintained as of `last_verified`. Multiple options often exist.
129
+ - `beta-unverified` — We have not researched a current canonical implementation. Install path needs your own due diligence.
130
+ - `use-at-your-own-risk` — Functions but carries significant risk. Currently only used for MCPs that violate the upstream service's ToS or carry account-ban risk (e.g., LinkedIn unofficial-API MCPs). Always read `risk_advisory` before installing.
131
+
101
132
  Server install details (package, runtime, auth method) live in the registry, not the config file. This keeps configs small and lets registry updates propagate without users regenerating their files.
102
133
 
103
134
  ### `agent_runtime`