tokrepo-mcp-server 2.0.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +17 -7
  2. package/bin/server.js +465 -24
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  > Search, browse, and install AI assets from [TokRepo](https://tokrepo.com) — the open registry for AI skills, prompts, MCP configs, scripts, and workflows.
4
4
 
5
- [![npm](https://img.shields.io/npm/v/@tokrepo/mcp-server)](https://www.npmjs.com/package/@tokrepo/mcp-server)
5
+ [![npm](https://img.shields.io/npm/v/tokrepo-mcp-server)](https://www.npmjs.com/package/tokrepo-mcp-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  ## Quick Start
9
9
 
10
10
  ### Claude Code
11
11
  ```bash
12
- claude mcp add tokrepo -- npx tokrepo-mcp-server
12
+ claude mcp add tokrepo -- npx -y tokrepo-mcp-server
13
13
  ```
14
14
 
15
15
  ### Cursor / Windsurf
@@ -19,7 +19,7 @@ Add to your MCP config (`~/.cursor/mcp.json`):
19
19
  "mcpServers": {
20
20
  "tokrepo": {
21
21
  "command": "npx",
22
- "args": ["@tokrepo/mcp-server"]
22
+ "args": ["-y", "tokrepo-mcp-server"]
23
23
  }
24
24
  }
25
25
  }
@@ -27,8 +27,8 @@ Add to your MCP config (`~/.cursor/mcp.json`):
27
27
 
28
28
  ### OpenAI Codex / Gemini CLI
29
29
  ```bash
30
- codex --mcp-server tokrepo -- npx tokrepo-mcp-server
31
- gemini settings mcp add tokrepo -- npx tokrepo-mcp-server
30
+ codex --mcp-server tokrepo -- npx -y tokrepo-mcp-server
31
+ gemini settings mcp add tokrepo -- npx -y tokrepo-mcp-server
32
32
  ```
33
33
 
34
34
  ## What It Does
@@ -38,7 +38,9 @@ Once connected, your AI assistant can:
38
38
  - **Search** 200+ curated AI assets by keyword or category
39
39
  - **Browse** trending assets, filter by type (MCP, Skill, Prompt, Agent, Script)
40
40
  - **Get details** — full documentation, install instructions, and metadata
41
- - **Install** — get raw content ready to save or execute
41
+ - **Plan before install** — get install plan v2 with policy decisions, rollback, and verification
42
+ - **Safe Codex install** — dry-run by default; risky assets must be staged or explicitly approved
43
+ - **Lifecycle control** — list installed assets, uninstall managed files, and roll back install sessions
42
44
 
43
45
  ## Available Tools
44
46
 
@@ -46,6 +48,12 @@ Once connected, your AI assistant can:
46
48
  |------|-------------|
47
49
  | `tokrepo_search` | Search assets by keyword and tag |
48
50
  | `tokrepo_detail` | Get full asset details by UUID |
51
+ | `tokrepo_install_plan` | Get agent-native install plan v2 |
52
+ | `tokrepo_codex_install` | Dry-run, stage, or install a Codex skill safely |
53
+ | `tokrepo_clone_plan` | Bulk profile clone dry-run plan |
54
+ | `tokrepo_installed` | List TokRepo-managed Codex installs |
55
+ | `tokrepo_uninstall` | Dry-run or remove a managed Codex install |
56
+ | `tokrepo_rollback` | Dry-run or roll back a prior Codex install session |
49
57
  | `tokrepo_install` | Get raw installable content |
50
58
  | `tokrepo_trending` | Browse popular/latest assets |
51
59
 
@@ -59,7 +67,9 @@ You: "What's trending on TokRepo?"
59
67
  AI: [calls tokrepo_trending] → Shows top assets by popularity
60
68
 
61
69
  You: "Install that cursor rules asset"
62
- AI: [calls tokrepo_install] → Returns raw content ready to save
70
+ AI: [calls tokrepo_install_plan] → Reviews policy and actions
71
+ AI: [calls tokrepo_codex_install with dry_run=false, confirm=true] → Writes only after explicit confirmation
72
+ AI: [calls tokrepo_rollback with dry_run=true] → Shows exactly what would be removed before rollback
63
73
  ```
64
74
 
65
75
  ## Why TokRepo?
package/bin/server.js CHANGED
@@ -7,25 +7,25 @@
7
7
  * Works with Claude Code, Cursor, Codex, Gemini CLI, and any MCP client.
8
8
  *
9
9
  * Usage:
10
- * claude mcp add tokrepo -- npx @tokrepo/mcp-server
11
- * npx @tokrepo/mcp-server
10
+ * claude mcp add tokrepo -- npx -y tokrepo-mcp-server
11
+ * npx -y tokrepo-mcp-server
12
12
  */
13
13
 
14
14
  const https = require('https');
15
- const readline = require('readline');
16
- const fs = require('fs');
17
- const path = require('path');
18
15
  const crypto = require('crypto');
16
+ const { execFile } = require('child_process');
19
17
 
20
18
  const API_BASE = process.env.TOKREPO_API || 'https://api.tokrepo.com';
21
19
  const TOKREPO_URL = 'https://tokrepo.com';
22
20
  const TOKREPO_TOKEN = process.env.TOKREPO_TOKEN || '';
21
+ const TOKREPO_CLI = process.env.TOKREPO_CLI || '';
22
+ const SERVER_VERSION = '2.2.0';
23
23
 
24
24
  // ─── MCP Protocol (JSON-RPC over stdio) ───
25
25
 
26
26
  const SERVER_INFO = {
27
27
  name: 'tokrepo',
28
- version: '2.0.0',
28
+ version: SERVER_VERSION,
29
29
  };
30
30
 
31
31
  const CAPABILITIES = {
@@ -53,6 +53,20 @@ const TOOLS = [
53
53
  description: 'Max results (default 10, max 20)',
54
54
  default: 10,
55
55
  },
56
+ target: {
57
+ type: 'string',
58
+ description: 'Optional agent target filter. Use codex for Codex-compatible assets.',
59
+ enum: ['codex'],
60
+ },
61
+ kind: {
62
+ type: 'string',
63
+ description: 'Optional asset kind filter, e.g. skill, prompt, knowledge, mcp_config, script',
64
+ },
65
+ policy: {
66
+ type: 'string',
67
+ description: 'Optional Codex install policy filter.',
68
+ enum: ['allow', 'confirm', 'stage_only', 'deny'],
69
+ },
56
70
  },
57
71
  required: ['query'],
58
72
  },
@@ -85,6 +99,152 @@ const TOOLS = [
85
99
  required: ['uuid'],
86
100
  },
87
101
  },
102
+ {
103
+ name: 'tokrepo_install_plan',
104
+ description: 'Return an agent-native install plan v2 for a TokRepo asset. Use this before installing: it includes preconditions, actions, risk profile, policy decision, rollback, and post-install verification.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ uuid: {
109
+ type: 'string',
110
+ description: 'Asset UUID, workflow URL slug, or workflow UUID from search/detail results',
111
+ },
112
+ target: {
113
+ type: 'string',
114
+ description: 'Install target. Currently codex is supported.',
115
+ enum: ['codex'],
116
+ default: 'codex',
117
+ },
118
+ },
119
+ required: ['uuid'],
120
+ },
121
+ },
122
+ {
123
+ name: 'tokrepo_codex_install',
124
+ description: 'Safely install a TokRepo asset into local Codex. Defaults to dry_run=true. To write files, set dry_run=false and confirm=true. Risky assets require stage=true or approve_risk=true.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ uuid: {
129
+ type: 'string',
130
+ description: 'Asset UUID, workflow URL, or search term accepted by the TokRepo CLI',
131
+ },
132
+ dry_run: {
133
+ type: 'boolean',
134
+ description: 'When true, return the plan only and do not write files. Default true.',
135
+ default: true,
136
+ },
137
+ stage: {
138
+ type: 'boolean',
139
+ description: 'Write only a staged install plan under ~/.codex/tokrepo/staged instead of activating a skill.',
140
+ default: false,
141
+ },
142
+ confirm: {
143
+ type: 'boolean',
144
+ description: 'Required when dry_run=false to prevent accidental writes.',
145
+ default: false,
146
+ },
147
+ approve_risk: {
148
+ type: 'boolean',
149
+ description: 'Required to activate assets whose policy decision is confirm or stage_only. Prefer stage=true for high-risk assets.',
150
+ default: false,
151
+ },
152
+ },
153
+ required: ['uuid'],
154
+ },
155
+ },
156
+ {
157
+ name: 'tokrepo_clone_plan',
158
+ description: 'Plan a bulk Codex install from a TokRepo user profile using the TokRepo CLI. Returns JSON dry-run output without writing files.',
159
+ inputSchema: {
160
+ type: 'object',
161
+ properties: {
162
+ user: {
163
+ type: 'string',
164
+ description: 'TokRepo username such as @henuwangkai or @me',
165
+ },
166
+ keyword: {
167
+ type: 'string',
168
+ description: 'Optional keyword filter, e.g. video',
169
+ },
170
+ types: {
171
+ type: 'string',
172
+ description: 'Optional comma-separated asset kinds, e.g. skill,prompt,knowledge',
173
+ },
174
+ },
175
+ required: ['user'],
176
+ },
177
+ },
178
+ {
179
+ name: 'tokrepo_installed',
180
+ description: 'List Codex assets installed by TokRepo from the local install manifest, including file status and session ids.',
181
+ inputSchema: {
182
+ type: 'object',
183
+ properties: {},
184
+ },
185
+ },
186
+ {
187
+ name: 'tokrepo_uninstall',
188
+ description: 'Safely uninstall a TokRepo-managed Codex asset. Defaults to dry_run=true. To remove files, set dry_run=false and confirm=true. Local changes are blocked unless force=true.',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ uuid: {
193
+ type: 'string',
194
+ description: 'Installed asset UUID, UUID prefix, or title.',
195
+ },
196
+ dry_run: {
197
+ type: 'boolean',
198
+ description: 'When true, return the removal plan without deleting files. Default true.',
199
+ default: true,
200
+ },
201
+ confirm: {
202
+ type: 'boolean',
203
+ description: 'Required when dry_run=false to prevent accidental deletes.',
204
+ default: false,
205
+ },
206
+ force: {
207
+ type: 'boolean',
208
+ description: 'Allow removal when local files changed since installation.',
209
+ default: false,
210
+ },
211
+ },
212
+ required: ['uuid'],
213
+ },
214
+ },
215
+ {
216
+ name: 'tokrepo_rollback',
217
+ description: 'Roll back a previous TokRepo Codex install session. Defaults to dry_run=true and last=true.',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ session_id: {
222
+ type: 'string',
223
+ description: 'Session id to roll back. Omit when last=true.',
224
+ },
225
+ last: {
226
+ type: 'boolean',
227
+ description: 'Use the latest install/stage session. Default true.',
228
+ default: true,
229
+ },
230
+ dry_run: {
231
+ type: 'boolean',
232
+ description: 'When true, return the rollback plan without deleting files. Default true.',
233
+ default: true,
234
+ },
235
+ confirm: {
236
+ type: 'boolean',
237
+ description: 'Required when dry_run=false to prevent accidental deletes.',
238
+ default: false,
239
+ },
240
+ force: {
241
+ type: 'boolean',
242
+ description: 'Allow rollback when local files changed since installation.',
243
+ default: false,
244
+ },
245
+ },
246
+ },
247
+ },
88
248
  {
89
249
  name: 'tokrepo_trending',
90
250
  description: 'Get trending/popular AI assets on TokRepo. Use when user asks for recommended or popular AI tools.',
@@ -107,22 +267,22 @@ const TOOLS = [
107
267
  },
108
268
  {
109
269
  name: 'tokrepo_push',
110
- description: 'Push files to TokRepo as an AI asset. Idempotent upsertwill create if new, update if title matches, skip if unchanged. Requires TOKREPO_TOKEN env var.',
270
+ description: 'Push ONE specific asset to TokRepo. You choose exactly which files to include nothing is uploaded automatically. Set visibility=0 for private (only you can see) or visibility=1 for public. IMPORTANT: Always confirm with the user before pushing, and never push files that may contain secrets, credentials, or personal data. Requires TOKREPO_TOKEN env var.',
111
271
  inputSchema: {
112
272
  type: 'object',
113
273
  properties: {
114
274
  title: {
115
275
  type: 'string',
116
- description: 'Asset title',
276
+ description: 'Asset title (descriptive name for this specific asset)',
117
277
  },
118
278
  files: {
119
279
  type: 'array',
120
- description: 'Array of files to push',
280
+ description: 'Only the specific files for THIS asset — not all project files. Each file you list will be uploaded.',
121
281
  items: {
122
282
  type: 'object',
123
283
  properties: {
124
284
  name: { type: 'string', description: 'File name (e.g. "rules.md")' },
125
- content: { type: 'string', description: 'File content' },
285
+ content: { type: 'string', description: 'File content — review for secrets before including' },
126
286
  type: { type: 'string', description: 'File type: skill, prompt, script, config, other', default: 'other' },
127
287
  },
128
288
  required: ['name', 'content'],
@@ -136,8 +296,8 @@ const TOOLS = [
136
296
  },
137
297
  visibility: {
138
298
  type: 'number',
139
- description: '0=private, 1=public (default 1)',
140
- default: 1,
299
+ description: '0 = private (only visible to you, safe default for personal assets), 1 = public (visible to everyone). When unsure, default to 0 (private).',
300
+ default: 0,
141
301
  },
142
302
  },
143
303
  required: ['title', 'files'],
@@ -247,7 +407,7 @@ function apiPost(urlPath, body, token) {
247
407
  'Content-Type': 'application/json',
248
408
  'Content-Length': Buffer.byteLength(bodyStr),
249
409
  'Authorization': `Bearer ${token}`,
250
- 'User-Agent': 'tokrepo-mcp-server/2.0.0',
410
+ 'User-Agent': `tokrepo-mcp-server/${SERVER_VERSION}`,
251
411
  },
252
412
  timeout: 15000,
253
413
  }, (res) => {
@@ -278,7 +438,7 @@ function apiGetAuth(urlPath, token) {
278
438
  headers: {
279
439
  'Accept': 'application/json',
280
440
  'Authorization': `Bearer ${token}`,
281
- 'User-Agent': 'tokrepo-mcp-server/2.0.0',
441
+ 'User-Agent': `tokrepo-mcp-server/${SERVER_VERSION}`,
282
442
  },
283
443
  timeout: 10000,
284
444
  }, (res) => {
@@ -300,35 +460,117 @@ function requireToken() {
300
460
  return TOKREPO_TOKEN;
301
461
  }
302
462
 
463
+ function workflowIdentifier(input) {
464
+ const raw = String(input || '').trim();
465
+ const match = raw.match(/workflows\/([^/?#]+)/);
466
+ const value = match ? match[1] : raw;
467
+ if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value)) {
468
+ return { param: 'uuid', value };
469
+ }
470
+ return { param: 'slug', value };
471
+ }
472
+
473
+ async function fetchInstallPlan(input, target = 'codex') {
474
+ const id = workflowIdentifier(input);
475
+ const params = new URLSearchParams({ target });
476
+ params.set(id.param, id.value);
477
+ let res = await apiGet(`/api/v1/tokenboard/workflows/install-plan?${params}`);
478
+ if (res.code === 200 && res.data?.plan) return res.data.plan;
479
+
480
+ if (id.param === 'slug') {
481
+ const search = await apiGet(`/api/v1/tokenboard/workflows/list?keyword=${encodeURIComponent(id.value.replace(/[-_.]/g, ' '))}&page=1&page_size=1&sort_by=views`);
482
+ const uuid = search.data?.list?.[0]?.uuid;
483
+ if (uuid) {
484
+ res = await apiGet(`/api/v1/tokenboard/workflows/install-plan?uuid=${encodeURIComponent(uuid)}&target=${encodeURIComponent(target)}`);
485
+ if (res.code === 200 && res.data?.plan) return res.data.plan;
486
+ }
487
+ }
488
+
489
+ throw new Error(res.message || `Install plan not found for ${input}`);
490
+ }
491
+
492
+ function planPolicyDecision(plan) {
493
+ const policy = plan?.policy_decision || plan?.policyDecision || {};
494
+ return String(policy.decision || 'confirm');
495
+ }
496
+
497
+ function runTokrepoCli(args) {
498
+ const command = TOKREPO_CLI || 'npx';
499
+ const finalArgs = TOKREPO_CLI ? args : ['-y', 'tokrepo@latest', ...args];
500
+ return new Promise((resolve, reject) => {
501
+ execFile(command, finalArgs, {
502
+ env: { ...process.env, TOKREPO_NONINTERACTIVE: '1' },
503
+ maxBuffer: 20 * 1024 * 1024,
504
+ timeout: 120000,
505
+ }, (err, stdout, stderr) => {
506
+ if (err) {
507
+ reject(new Error(`${err.message}${stderr ? `\n${stderr}` : ''}`));
508
+ return;
509
+ }
510
+ resolve({ stdout, stderr });
511
+ });
512
+ });
513
+ }
514
+
515
+ function jsonText(title, data) {
516
+ return `${title}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
517
+ }
518
+
303
519
  // ─── Tool Handlers ───
304
520
 
305
521
  async function handleSearch(args) {
306
- const { query, tag, limit = 10 } = args;
522
+ const { query, tag, limit = 10, target = '', kind = '', policy = '' } = args;
523
+ if (target || kind || policy) {
524
+ const cliArgs = ['search', query, '--json', '--page-size', String(Math.min(limit, 20))];
525
+ if (target) cliArgs.push('--target', target);
526
+ if (kind) cliArgs.push('--kind', kind);
527
+ if (policy) cliArgs.push('--policy', policy);
528
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
529
+ let data;
530
+ try {
531
+ data = JSON.parse(stdout);
532
+ } catch {
533
+ data = { stdout, stderr };
534
+ }
535
+ return { content: [{ type: 'text', text: jsonText('Filtered TokRepo search results', data) }] };
536
+ }
537
+
538
+ // Normalize: hyphens/underscores/dots → spaces for better matching
539
+ const normalized = query.replace(/[-_.]/g, ' ').replace(/\s+/g, ' ').trim();
307
540
  const params = new URLSearchParams({
308
- keyword: query,
541
+ keyword: normalized,
309
542
  page: '1',
310
543
  page_size: String(Math.min(limit, 20)),
311
544
  sort_by: 'popular',
312
545
  });
313
546
  if (tag) {
314
- // Map tag name to tag_id (approximate)
315
547
  const tagMap = { agent: 11, coding: 7, efficiency: 10, 'cost-saving': 12, methodology: 15, 'data-analysis': 14, writing: 1, marketing: 16, learning: 17, research: 8 };
316
548
  if (tagMap[tag]) params.set('tag_id', String(tagMap[tag]));
317
549
  }
318
550
 
319
551
  const res = await apiGet(`/api/v1/tokenboard/workflows/list?${params}`);
320
552
  if (res.code !== 200 || !res.data?.list?.length) {
321
- return { content: [{ type: 'text', text: `No assets found for "${query}". Try broader keywords.` }] };
553
+ // Suggest broader terms when no results
554
+ const words = normalized.split(' ');
555
+ let hint = 'Try broader keywords.';
556
+ if (words.length > 1) {
557
+ hint = `Try: "${words[0]}" or "${words.slice(0, 2).join(' ')}"`;
558
+ }
559
+ return { content: [{ type: 'text', text: `No assets found for "${query}". ${hint}` }] };
322
560
  }
323
561
 
324
562
  const items = res.data.list.slice(0, limit);
325
563
  const lines = items.map((item, i) => {
326
564
  const tags = (item.tags || []).map(t => t.name || t.slug).join(', ');
565
+ // Truncate description to keep agent context concise
566
+ let desc = item.description || '';
567
+ if (desc.length > 120) desc = desc.substring(0, 117) + '...';
327
568
  return [
328
569
  `${i + 1}. **${item.title}**`,
329
- ` ${item.description || ''}`,
570
+ ` ${desc}`,
330
571
  ` Tags: ${tags || 'general'} | ★ ${item.vote_count || 0} | 👁 ${item.view_count || 0}`,
331
- ` Install: \`tokrepo install ${item.uuid}\``,
572
+ ` Plan: call \`tokrepo_install_plan\` with uuid \`${item.uuid}\``,
573
+ ` Install: \`tokrepo install ${item.uuid} --target codex --dry-run --json\``,
332
574
  ` URL: ${TOKREPO_URL}/en/workflows/${item.uuid}`,
333
575
  ].join('\n');
334
576
  });
@@ -359,7 +601,8 @@ async function handleDetail(args) {
359
601
  `**Stars**: ${w.vote_count || 0} | **Views**: ${w.view_count || 0} | **Forks**: ${w.fork_count || 0}`,
360
602
  `**Author**: ${w.author_name || 'Anonymous'}`,
361
603
  `**URL**: ${TOKREPO_URL}/en/workflows/${w.uuid}`,
362
- `**Install**: \`tokrepo install ${w.uuid}\``,
604
+ `**Plan**: call \`tokrepo_install_plan\` with uuid \`${w.uuid}\` before installing`,
605
+ `**Install**: \`tokrepo install ${w.uuid} --target codex --dry-run --json\``,
363
606
  ``,
364
607
  steps,
365
608
  ].join('\n');
@@ -380,6 +623,186 @@ async function handleInstall(args) {
380
623
  }
381
624
  }
382
625
 
626
+ async function handleInstallPlan(args) {
627
+ const { uuid, target = 'codex' } = args;
628
+ const plan = await fetchInstallPlan(uuid, target);
629
+ const decision = planPolicyDecision(plan);
630
+ const command = decision === 'allow'
631
+ ? `tokrepo install ${plan.asset_uuid || uuid} --target codex --yes`
632
+ : `tokrepo install ${plan.asset_uuid || uuid} --target codex --dry-run --json`;
633
+ return {
634
+ content: [{
635
+ type: 'text',
636
+ text: jsonText(`Install plan v${plan.schema_version || 1} for ${plan.asset_title || uuid}\n\nPolicy: ${decision}\nCLI: ${command}`, plan),
637
+ }],
638
+ };
639
+ }
640
+
641
+ async function handleCodexInstall(args) {
642
+ const {
643
+ uuid,
644
+ dry_run = true,
645
+ stage = false,
646
+ confirm = false,
647
+ approve_risk = false,
648
+ } = args;
649
+
650
+ const plan = await fetchInstallPlan(uuid, 'codex');
651
+ const decision = planPolicyDecision(plan);
652
+ if (dry_run !== false) {
653
+ const cliArgs = ['install', plan.asset_uuid || uuid, '--target', 'codex', '--dry-run', '--json'];
654
+ if (stage) cliArgs.push('--stage');
655
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
656
+ let data;
657
+ try {
658
+ data = JSON.parse(stdout);
659
+ } catch {
660
+ data = { stdout, stderr };
661
+ }
662
+ return {
663
+ content: [{
664
+ type: 'text',
665
+ text: jsonText(`Dry run only. Policy: ${decision}. Set dry_run=false and confirm=true to write files.`, data),
666
+ }],
667
+ };
668
+ }
669
+
670
+ if (!confirm) {
671
+ const cliArgs = ['install', plan.asset_uuid || uuid, '--target', 'codex', '--dry-run', '--json'];
672
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
673
+ let data;
674
+ try {
675
+ data = JSON.parse(stdout);
676
+ } catch {
677
+ data = { stdout, stderr };
678
+ }
679
+ return {
680
+ isError: true,
681
+ content: [{
682
+ type: 'text',
683
+ text: jsonText('Refused to write files because confirm=true was not provided. Dry-run plan follows.', data),
684
+ }],
685
+ };
686
+ }
687
+ if (decision === 'deny') {
688
+ return {
689
+ isError: true,
690
+ content: [{ type: 'text', text: jsonText('Install policy denied this asset.', plan) }],
691
+ };
692
+ }
693
+ if ((decision === 'confirm' || decision === 'stage_only') && !stage && !approve_risk) {
694
+ return {
695
+ isError: true,
696
+ content: [{
697
+ type: 'text',
698
+ text: jsonText(`Policy is ${decision}. Re-run with stage=true to avoid activation, or approve_risk=true to activate anyway.`, plan),
699
+ }],
700
+ };
701
+ }
702
+
703
+ const cliArgs = ['install', plan.asset_uuid || uuid, '--target', 'codex', '--json', '--yes'];
704
+ if (stage) cliArgs.push('--stage');
705
+ if (approve_risk) cliArgs.push('--approve-mcp');
706
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
707
+ let data;
708
+ try {
709
+ data = JSON.parse(stdout);
710
+ } catch {
711
+ data = { stdout, stderr };
712
+ }
713
+ return { content: [{ type: 'text', text: jsonText('Codex install result', data) }] };
714
+ }
715
+
716
+ async function handleClonePlan(args) {
717
+ const { user, keyword = '', types = '' } = args;
718
+ const cliArgs = ['clone', user, '--target', 'codex', '--dry-run', '--json'];
719
+ if (keyword) cliArgs.push('--keyword', keyword);
720
+ if (types) cliArgs.push('--types', types);
721
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
722
+ let data;
723
+ try {
724
+ data = JSON.parse(stdout);
725
+ } catch {
726
+ data = { stdout, stderr };
727
+ }
728
+ return { content: [{ type: 'text', text: jsonText('Bulk Codex clone dry-run plan', data) }] };
729
+ }
730
+
731
+ async function handleInstalled() {
732
+ const { stdout, stderr } = await runTokrepoCli(['installed', '--target', 'codex', '--json']);
733
+ let data;
734
+ try {
735
+ data = JSON.parse(stdout);
736
+ } catch {
737
+ data = { stdout, stderr };
738
+ }
739
+ return { content: [{ type: 'text', text: jsonText('TokRepo Codex installed assets', data) }] };
740
+ }
741
+
742
+ async function handleUninstall(args) {
743
+ const { uuid, dry_run = true, confirm = false, force = false } = args;
744
+ const cliArgs = ['uninstall', uuid, '--target', 'codex', '--json'];
745
+ if (dry_run !== false) cliArgs.push('--dry-run');
746
+ if (force) cliArgs.push('--force');
747
+
748
+ if (dry_run === false && !confirm) {
749
+ cliArgs.push('--dry-run');
750
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
751
+ let data;
752
+ try {
753
+ data = JSON.parse(stdout);
754
+ } catch {
755
+ data = { stdout, stderr };
756
+ }
757
+ return {
758
+ isError: true,
759
+ content: [{ type: 'text', text: jsonText('Refused to uninstall because confirm=true was not provided. Dry-run plan follows.', data) }],
760
+ };
761
+ }
762
+
763
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
764
+ let data;
765
+ try {
766
+ data = JSON.parse(stdout);
767
+ } catch {
768
+ data = { stdout, stderr };
769
+ }
770
+ return { content: [{ type: 'text', text: jsonText(dry_run === false ? 'TokRepo Codex uninstall result' : 'TokRepo Codex uninstall dry-run', data) }] };
771
+ }
772
+
773
+ async function handleRollback(args) {
774
+ const { session_id = '', last = true, dry_run = true, confirm = false, force = false } = args;
775
+ const cliArgs = ['rollback', '--target', 'codex', '--json'];
776
+ if (session_id) cliArgs.push(session_id);
777
+ else if (last !== false) cliArgs.push('--last');
778
+ if (dry_run !== false) cliArgs.push('--dry-run');
779
+ if (force) cliArgs.push('--force');
780
+
781
+ if (dry_run === false && !confirm) {
782
+ cliArgs.push('--dry-run');
783
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
784
+ let data;
785
+ try {
786
+ data = JSON.parse(stdout);
787
+ } catch {
788
+ data = { stdout, stderr };
789
+ }
790
+ return {
791
+ isError: true,
792
+ content: [{ type: 'text', text: jsonText('Refused to roll back because confirm=true was not provided. Dry-run plan follows.', data) }],
793
+ };
794
+ }
795
+
796
+ const { stdout, stderr } = await runTokrepoCli(cliArgs);
797
+ let data;
798
+ try {
799
+ data = JSON.parse(stdout);
800
+ } catch {
801
+ data = { stdout, stderr };
802
+ }
803
+ return { content: [{ type: 'text', text: jsonText(dry_run === false ? 'TokRepo Codex rollback result' : 'TokRepo Codex rollback dry-run', data) }] };
804
+ }
805
+
383
806
  async function handleTrending(args) {
384
807
  const { sort = 'popular', limit = 10 } = args;
385
808
  const params = new URLSearchParams({
@@ -528,6 +951,12 @@ async function handleRequest(msg) {
528
951
  case 'tokrepo_search': result = await handleSearch(args || {}); break;
529
952
  case 'tokrepo_detail': result = await handleDetail(args || {}); break;
530
953
  case 'tokrepo_install': result = await handleInstall(args || {}); break;
954
+ case 'tokrepo_install_plan': result = await handleInstallPlan(args || {}); break;
955
+ case 'tokrepo_codex_install': result = await handleCodexInstall(args || {}); break;
956
+ case 'tokrepo_clone_plan': result = await handleClonePlan(args || {}); break;
957
+ case 'tokrepo_installed': result = await handleInstalled(args || {}); break;
958
+ case 'tokrepo_uninstall': result = await handleUninstall(args || {}); break;
959
+ case 'tokrepo_rollback': result = await handleRollback(args || {}); break;
531
960
  case 'tokrepo_trending': result = await handleTrending(args || {}); break;
532
961
  case 'tokrepo_push': result = await handlePush(args || {}); break;
533
962
  case 'tokrepo_status': result = await handleStatus(args || {}); break;
@@ -551,8 +980,13 @@ async function handleRequest(msg) {
551
980
  // ─── Stdio Transport ───
552
981
 
553
982
  function main() {
554
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
555
983
  let buffer = '';
984
+ let pending = 0;
985
+ let inputEnded = false;
986
+
987
+ const maybeExit = () => {
988
+ if (inputEnded && pending === 0) process.exit(0);
989
+ };
556
990
 
557
991
  process.stdin.on('data', (chunk) => {
558
992
  buffer += chunk.toString();
@@ -564,6 +998,7 @@ function main() {
564
998
  if (!trimmed) continue;
565
999
  try {
566
1000
  const msg = JSON.parse(trimmed);
1001
+ pending++;
567
1002
  handleRequest(msg).then((response) => {
568
1003
  if (response) {
569
1004
  process.stdout.write(JSON.stringify(response) + '\n');
@@ -574,6 +1009,9 @@ function main() {
574
1009
  id: msg.id || null,
575
1010
  error: { code: -32603, message: e.message },
576
1011
  }) + '\n');
1012
+ }).finally(() => {
1013
+ pending--;
1014
+ maybeExit();
577
1015
  });
578
1016
  } catch (e) {
579
1017
  // Skip malformed JSON
@@ -581,10 +1019,13 @@ function main() {
581
1019
  }
582
1020
  });
583
1021
 
584
- process.stdin.on('end', () => process.exit(0));
1022
+ process.stdin.on('end', () => {
1023
+ inputEnded = true;
1024
+ maybeExit();
1025
+ });
585
1026
 
586
1027
  // Log to stderr (not stdout, which is the MCP transport)
587
- process.stderr.write(`TokRepo MCP Server v2.0.0 started${TOKREPO_TOKEN ? ' (authenticated)' : ' (read-only, set TOKREPO_TOKEN for write access)'}\n`);
1028
+ process.stderr.write(`TokRepo MCP Server v${SERVER_VERSION} started${TOKREPO_TOKEN ? ' (authenticated)' : ' (read-only, set TOKREPO_TOKEN for write access)'}\n`);
588
1029
  }
589
1030
 
590
1031
  main();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokrepo-mcp-server",
3
- "version": "2.0.0",
4
- "description": "MCP server for TokRepo — search, browse, install, and push AI assets from any MCP client (Claude Code, Cursor, Codex, Gemini CLI).",
3
+ "version": "2.2.0",
4
+ "description": "Agent-native MCP server for TokRepo — search, plan, safely install, and push AI assets from MCP clients.",
5
5
  "mcpName": "io.github.tokrepo/mcp-server",
6
6
  "bin": {
7
7
  "tokrepo-mcp-server": "bin/server.js"