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.
- package/README.md +17 -7
- package/bin/server.js +465 -24
- 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
|
-
[](https://www.npmjs.com/package/tokrepo-mcp-server)
|
|
6
6
|
[](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": ["
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
11
|
-
* npx
|
|
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:
|
|
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
|
|
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: '
|
|
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
|
|
140
|
-
default:
|
|
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':
|
|
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':
|
|
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:
|
|
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
|
-
|
|
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
|
-
` ${
|
|
570
|
+
` ${desc}`,
|
|
330
571
|
` Tags: ${tags || 'general'} | ★ ${item.vote_count || 0} | 👁 ${item.view_count || 0}`,
|
|
331
|
-
`
|
|
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
|
-
`**
|
|
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', () =>
|
|
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
|
|
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.
|
|
4
|
-
"description": "MCP server for TokRepo — search,
|
|
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"
|