tokrepo-mcp-server 2.5.0 → 2.6.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 +5 -3
  2. package/bin/server.js +302 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -35,7 +35,8 @@ gemini settings mcp add tokrepo -- npx -y tokrepo-mcp-server
35
35
 
36
36
  Once connected, your AI assistant can:
37
37
 
38
- - **Search** 200+ curated AI assets by keyword or category with Codex-first `agent_fit` ranking
38
+ - **Discover during planning** turn a task or capability gap into structured candidate skills, prompts, MCP configs, scripts, and workflows
39
+ - **Search** 200+ curated AI assets by keyword or category with agent fit signals
39
40
  - **Browse** trending assets, filter by type (MCP, Skill, Prompt, Agent, Script)
40
41
  - **Get details** — full documentation, install instructions, and metadata
41
42
  - **Plan before install** — get install plan v2 with policy decisions, rollback, and verification
@@ -46,6 +47,7 @@ Once connected, your AI assistant can:
46
47
 
47
48
  | Tool | Description |
48
49
  |------|-------------|
50
+ | `tokrepo_discover` | Planning-time capability discovery from a task, environment, and constraints |
49
51
  | `tokrepo_search` | Search assets by keyword/tag with `agent_fit` ranking |
50
52
  | `tokrepo_detail` | Get full asset details by UUID |
51
53
  | `tokrepo_install_plan` | Get agent-native install plan v2 |
@@ -60,10 +62,10 @@ Once connected, your AI assistant can:
60
62
 
61
63
  ```
62
64
  You: "Find me a good MCP server for databases"
63
- AI: [calls tokrepo_search] → Shows DBHub, Supabase MCP, PostgreSQL MCP with install commands
65
+ AI: [calls tokrepo_discover] → Ranks DBHub, Supabase MCP, PostgreSQL MCP as candidate capabilities
64
66
 
65
67
  You: "What video assets should I install?"
66
- AI: [calls tokrepo_search] → Ranks native Codex assets by `agent_fit`
68
+ AI: [calls tokrepo_discover] → Finds relevant skills, checks fit and policy, then asks before installing
67
69
 
68
70
  You: "Install that cursor rules asset"
69
71
  AI: [calls tokrepo_install_plan] → Reviews policy and actions
package/bin/server.js CHANGED
@@ -19,7 +19,7 @@ const API_BASE = process.env.TOKREPO_API || 'https://api.tokrepo.com';
19
19
  const TOKREPO_URL = 'https://tokrepo.com';
20
20
  const TOKREPO_TOKEN = process.env.TOKREPO_TOKEN || '';
21
21
  const TOKREPO_CLI = process.env.TOKREPO_CLI || '';
22
- const SERVER_VERSION = '2.5.0';
22
+ const SERVER_VERSION = '2.6.0';
23
23
 
24
24
  // ─── MCP Protocol (JSON-RPC over stdio) ───
25
25
 
@@ -33,9 +33,44 @@ const CAPABILITIES = {
33
33
  };
34
34
 
35
35
  const TOOLS = [
36
+ {
37
+ name: 'tokrepo_discover',
38
+ description: 'Planning-time capability discovery for agents. Call this during autonomous planning when the task may need a reusable skill, MCP config, prompt, script, workflow, or other AI asset. Returns structured candidates, fit signals, and next MCP calls.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ task: {
43
+ type: 'string',
44
+ description: 'The user goal, subtask, or capability gap the agent is planning for.',
45
+ },
46
+ target: {
47
+ type: 'string',
48
+ description: 'Optional agent/runtime target. Use "any" for generic discovery.',
49
+ enum: ['any', 'codex', 'claude_code', 'gemini_cli', 'cursor', 'windsurf', 'mcp_client'],
50
+ default: 'any',
51
+ },
52
+ environment: {
53
+ type: 'object',
54
+ description: 'Optional local environment signals, such as project_type, frameworks, language, can_run_shell, can_write_files, or browser_available.',
55
+ additionalProperties: true,
56
+ },
57
+ constraints: {
58
+ type: 'object',
59
+ description: 'Optional constraints such as kind, policy, risk, language, prefer_verified, or must_not_modify_files.',
60
+ additionalProperties: true,
61
+ },
62
+ limit: {
63
+ type: 'number',
64
+ description: 'Max candidates (default 6, max 10)',
65
+ default: 6,
66
+ },
67
+ },
68
+ required: ['task'],
69
+ },
70
+ },
36
71
  {
37
72
  name: 'tokrepo_search',
38
- description: 'Search TokRepo for AI assets (skills, prompts, MCP configs, scripts, workflows). Returns matching assets with titles, descriptions, tags, stars, and install commands. Use this when the user asks to find AI tools, MCP servers, Claude skills, prompts, or workflows.',
73
+ description: 'Search TokRepo for AI assets (skills, prompts, MCP configs, scripts, workflows). Returns matching assets with titles, descriptions, tags, stars, and install commands. Use this when the user asks to find AI tools, MCP servers, skills, prompts, or workflows.',
39
74
  inputSchema: {
40
75
  type: 'object',
41
76
  properties: {
@@ -55,9 +90,8 @@ const TOOLS = [
55
90
  },
56
91
  target: {
57
92
  type: 'string',
58
- description: 'Agent target filter. Defaults to codex so results include agent_fit ranking for installable Codex assets.',
59
- enum: ['codex'],
60
- default: 'codex',
93
+ description: 'Optional agent target filter. Use "any" or omit it for generic discovery.',
94
+ enum: ['any', 'codex', 'claude_code', 'gemini_cli', 'cursor', 'windsurf', 'mcp_client'],
61
95
  },
62
96
  kind: {
63
97
  type: 'string',
@@ -112,8 +146,8 @@ const TOOLS = [
112
146
  },
113
147
  target: {
114
148
  type: 'string',
115
- description: 'Install target. Currently codex is supported.',
116
- enum: ['codex'],
149
+ description: 'Install target adapter. Codex is native; other adapters may return planned or staged contracts as they become available.',
150
+ enum: ['codex', 'claude_code', 'gemini_cli'],
117
151
  default: 'codex',
118
152
  },
119
153
  },
@@ -397,6 +431,7 @@ const TOOLS = [
397
431
  ];
398
432
 
399
433
  const EXPOSED_TOOL_NAMES = new Set([
434
+ 'tokrepo_discover',
400
435
  'tokrepo_search',
401
436
  'tokrepo_detail',
402
437
  'tokrepo_install_plan',
@@ -577,13 +612,255 @@ function jsonText(title, data) {
577
612
  return `${title}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
578
613
  }
579
614
 
615
+ function compactText(value, maxLength = 240) {
616
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
617
+ if (text.length <= maxLength) return text;
618
+ return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
619
+ }
620
+
621
+ function clampLimit(value, fallback, max) {
622
+ const parsed = Number(value);
623
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
624
+ return Math.min(Math.floor(parsed), max);
625
+ }
626
+
627
+ function asArray(value) {
628
+ if (Array.isArray(value)) return value.filter(Boolean);
629
+ if (value === undefined || value === null || value === '') return [];
630
+ return [value];
631
+ }
632
+
633
+ function normalizeTarget(value) {
634
+ const raw = compactText(value || 'any', 64).toLowerCase().replace(/[-\s]+/g, '_');
635
+ const aliases = {
636
+ claude: 'claude_code',
637
+ claude_code: 'claude_code',
638
+ gemini: 'gemini_cli',
639
+ gemini_cli: 'gemini_cli',
640
+ codex: 'codex',
641
+ cursor: 'cursor',
642
+ windsurf: 'windsurf',
643
+ mcp: 'mcp_client',
644
+ mcp_client: 'mcp_client',
645
+ any: 'any',
646
+ all: 'any',
647
+ };
648
+ return aliases[raw] || raw || 'any';
649
+ }
650
+
651
+ function itemTags(item) {
652
+ return asArray(item.tags).map(tag => compactText(tag.name || tag.slug || tag, 64)).filter(Boolean);
653
+ }
654
+
655
+ function itemAgentMetadata(item) {
656
+ return item.agent_metadata || item.agentMetadata || item.metadata?.agent_metadata || {};
657
+ }
658
+
659
+ function itemAgentFit(item) {
660
+ return item.agent_fit || item.agentFit || item.fit || {};
661
+ }
662
+
663
+ function candidateUuid(item) {
664
+ return compactText(item.uuid || item.workflow_uuid || item.workflowUuid || item.id, 128);
665
+ }
666
+
667
+ function candidateKind(item, metadata, fit) {
668
+ return compactText(item.asset_kind || item.assetKind || metadata.asset_kind || fit.asset_kind || '', 64);
669
+ }
670
+
671
+ function candidateTargets(item, metadata) {
672
+ const targets = [
673
+ ...asArray(item.target_tools || item.targetTools),
674
+ ...asArray(metadata.target_tools || metadata.targetTools),
675
+ ].map(target => compactText(target, 64)).filter(Boolean);
676
+ return [...new Set(targets)];
677
+ }
678
+
679
+ function extractSearchTerms(value, maxTerms = 8) {
680
+ const text = compactText(value, 240);
681
+ const stopWords = new Set([
682
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'for', 'from', 'how',
683
+ 'i', 'in', 'into', 'is', 'it', 'me', 'my', 'of', 'on', 'or', 'our', 'the', 'this',
684
+ 'to', 'use', 'with', 'fix', 'make', 'need', 'needs', 'want', 'issue', 'issues',
685
+ ]);
686
+ const words = text
687
+ .split(/[^a-zA-Z0-9+#.]+/)
688
+ .map(word => word.trim().toLowerCase())
689
+ .filter(word => word.length >= 2 && !stopWords.has(word));
690
+ const unique = [...new Set(words)].slice(0, maxTerms);
691
+ return unique.length ? unique : [compactText(text, 80)];
692
+ }
693
+
694
+ function buildDiscoveryQuery(task, environment, constraints) {
695
+ const parts = extractSearchTerms(task, 6);
696
+ for (const key of ['project_type', 'language', 'framework', 'domain']) {
697
+ if (environment?.[key]) parts.push(...extractSearchTerms(environment[key], 2));
698
+ }
699
+ for (const value of asArray(environment?.frameworks)) parts.push(...extractSearchTerms(value, 2));
700
+ if (constraints?.kind) parts.push(...extractSearchTerms(constraints.kind, 2));
701
+ return compactText([...new Set(parts)].join(' '), 100);
702
+ }
703
+
704
+ function buildCandidate(item, target) {
705
+ const uuid = candidateUuid(item);
706
+ const metadata = itemAgentMetadata(item);
707
+ const fit = itemAgentFit(item);
708
+ const kind = candidateKind(item, metadata, fit);
709
+ const targets = candidateTargets(item, metadata);
710
+ const planTarget = target && target !== 'any' && ['codex', 'claude_code', 'gemini_cli'].includes(target)
711
+ ? target
712
+ : 'codex';
713
+ const urlSlug = compactText(item.slug || uuid, 180);
714
+ const score = Number.isFinite(Number(fit.score)) ? Number(fit.score) : null;
715
+ const why = asArray(fit.why).map(reason => compactText(reason, 160)).filter(Boolean);
716
+
717
+ return {
718
+ uuid,
719
+ slug: compactText(item.slug || '', 180),
720
+ title: compactText(item.title, 160),
721
+ description: compactText(item.description || item.summary || '', 320),
722
+ url: urlSlug ? `${TOKREPO_URL}/en/workflows/${urlSlug}` : '',
723
+ tags: itemTags(item),
724
+ capability: {
725
+ kind,
726
+ install_mode: compactText(item.install_mode || item.installMode || metadata.install_mode || fit.install_mode || '', 64),
727
+ entrypoint: compactText(item.entrypoint || metadata.entrypoint || '', 160),
728
+ target_tools: targets,
729
+ },
730
+ fit: {
731
+ target: compactText(fit.target || target || 'any', 64),
732
+ score,
733
+ status: compactText(fit.status || item.agent_status || '', 64),
734
+ policy: compactText(fit.policy || item.policy || '', 64),
735
+ why,
736
+ },
737
+ next_mcp_calls: [
738
+ { tool: 'tokrepo_detail', arguments: { uuid } },
739
+ { tool: 'tokrepo_install_plan', arguments: { uuid, target: planTarget } },
740
+ ],
741
+ commands: {
742
+ inspect: `npx tokrepo detail ${uuid} --json`,
743
+ dry_run_install: planTarget === 'codex'
744
+ ? `npx tokrepo install ${uuid} --dry-run --json`
745
+ : `npx tokrepo install ${uuid} --target ${planTarget} --dry-run --json`,
746
+ },
747
+ agent_use_contract: [
748
+ 'Use only if the capability matches the current subtask.',
749
+ 'Call tokrepo_detail before install to inspect content and metadata.',
750
+ 'Call tokrepo_install_plan and respect policy_decision before writing files.',
751
+ 'Prefer dry-run or stage-only when risk or fit is uncertain.',
752
+ 'After using it, verify the original task outcome and record failures.',
753
+ ],
754
+ };
755
+ }
756
+
580
757
  // ─── Tool Handlers ───
581
758
 
759
+ async function handleDiscover(args) {
760
+ const task = compactText(args.task, 500);
761
+ if (!task) {
762
+ return {
763
+ content: [{ type: 'text', text: 'task is required for tokrepo_discover' }],
764
+ isError: true,
765
+ };
766
+ }
767
+
768
+ const environment = args.environment && typeof args.environment === 'object' ? args.environment : {};
769
+ const constraints = args.constraints && typeof args.constraints === 'object' ? args.constraints : {};
770
+ const target = normalizeTarget(args.target || constraints.target || 'any');
771
+ const limit = clampLimit(args.limit, 6, 10);
772
+ const kind = compactText(args.kind || constraints.kind || '', 64);
773
+ const policy = compactText(args.policy || constraints.policy || '', 64);
774
+ const query = buildDiscoveryQuery(task, environment, constraints);
775
+
776
+ let items = [];
777
+ let discoveryError = '';
778
+ let selectedQuery = query;
779
+ const taskTerms = extractSearchTerms(task, 6);
780
+ const attempts = [
781
+ { query, kind, policy },
782
+ { query: taskTerms.slice(0, 3).join(' '), kind, policy },
783
+ { query: taskTerms.slice(0, 2).join(' '), kind: '', policy: '' },
784
+ { query: taskTerms[0] || query, kind: '', policy: '' },
785
+ ].filter(attempt => attempt.query);
786
+ const seenAttempts = new Set();
787
+ const errors = [];
788
+
789
+ for (const attempt of attempts) {
790
+ const key = `${attempt.query}|${attempt.kind}|${attempt.policy}`;
791
+ if (seenAttempts.has(key)) continue;
792
+ seenAttempts.add(key);
793
+ const params = new URLSearchParams({
794
+ keyword: attempt.query,
795
+ page: '1',
796
+ page_size: String(limit),
797
+ sort_by: 'popular',
798
+ });
799
+ if (target && target !== 'any') params.set('target', target);
800
+ if (attempt.kind) params.set('kind', attempt.kind);
801
+ if (attempt.policy) params.set('policy', attempt.policy);
802
+
803
+ try {
804
+ const res = await apiGet(`/api/v1/tokenboard/workflows/list?${params}`);
805
+ items = res.code === 200 ? (res.data?.list || res.data?.items || []) : [];
806
+ if (res.code !== 200) errors.push(compactText(res.message || `API returned code ${res.code}`, 200));
807
+ if (items.length) {
808
+ selectedQuery = attempt.query;
809
+ break;
810
+ }
811
+ } catch (e) {
812
+ errors.push(compactText(e.message, 200));
813
+ }
814
+ }
815
+ discoveryError = errors.find(Boolean) || '';
816
+ const payload = {
817
+ schema_version: 1,
818
+ intent: {
819
+ task,
820
+ target,
821
+ query: selectedQuery,
822
+ queries_tried: [...seenAttempts].map(key => key.split('|')[0]),
823
+ environment,
824
+ constraints,
825
+ },
826
+ recommended_flow: [
827
+ 'During planning, call tokrepo_discover when the task exposes a capability gap.',
828
+ 'Rank candidates by fit, policy, trust, and whether the entrypoint matches the active agent runtime.',
829
+ 'Call tokrepo_detail for the top candidate before installation or use.',
830
+ 'Call tokrepo_install_plan and respect policy_decision, rollback, and verification steps.',
831
+ 'Dry-run or stage when the asset may write files, execute code, require secrets, or change global config.',
832
+ 'Use the installed capability only for the matching subtask, then verify the user goal.',
833
+ 'If the agent creates a reusable improvement, ask before publishing and use tokrepo_push with explicit files.',
834
+ ],
835
+ candidates: items.slice(0, limit).map(item => buildCandidate(item, target)).filter(candidate => candidate.uuid),
836
+ empty_state: items.length ? null : {
837
+ message: discoveryError
838
+ ? `TokRepo discovery could not fetch live candidates for "${selectedQuery}".`
839
+ : `No TokRepo candidates found for "${selectedQuery}".`,
840
+ error: discoveryError || undefined,
841
+ suggested_queries: [
842
+ task.split(/\s+/).slice(0, 4).join(' '),
843
+ compactText([kind, environment.project_type, environment.language].filter(Boolean).join(' '), 120),
844
+ 'agent skill workflow',
845
+ ].filter(Boolean),
846
+ },
847
+ };
848
+
849
+ return {
850
+ content: [{
851
+ type: 'text',
852
+ text: jsonText('TokRepo planning-time discovery', payload),
853
+ }],
854
+ };
855
+ }
856
+
582
857
  async function handleSearch(args) {
583
- const { query, tag, limit = 10, target = 'codex', kind = '', policy = '' } = args;
584
- if (target || kind || policy) {
858
+ const { query, tag, limit = 10, target = '', kind = '', policy = '' } = args;
859
+ const normalizedTarget = normalizeTarget(target || 'any');
860
+ const targetFilter = normalizedTarget === 'any' ? '' : normalizedTarget;
861
+ if (targetFilter || kind || policy) {
585
862
  const cliArgs = ['search', query, '--json', '--page-size', String(Math.min(limit, 20))];
586
- if (target) cliArgs.push('--target', target);
863
+ if (targetFilter) cliArgs.push('--target', targetFilter);
587
864
  if (kind) cliArgs.push('--kind', kind);
588
865
  if (policy) cliArgs.push('--policy', policy);
589
866
  const { stdout, stderr } = await runTokrepoCli(cliArgs);
@@ -631,7 +908,7 @@ async function handleSearch(args) {
631
908
  ` ${desc}`,
632
909
  ` Tags: ${tags || 'general'} | ★ ${item.vote_count || 0} | 👁 ${item.view_count || 0}`,
633
910
  ` Plan: call \`tokrepo_install_plan\` with uuid \`${item.uuid}\``,
634
- ` Install: \`tokrepo install ${item.uuid} --target codex --dry-run --json\``,
911
+ ` Install: \`tokrepo install ${item.uuid} --dry-run --json\``,
635
912
  ` URL: ${TOKREPO_URL}/en/workflows/${item.uuid}`,
636
913
  ].join('\n');
637
914
  });
@@ -663,7 +940,7 @@ async function handleDetail(args) {
663
940
  `**Author**: ${w.author_name || 'Anonymous'}`,
664
941
  `**URL**: ${TOKREPO_URL}/en/workflows/${w.uuid}`,
665
942
  `**Plan**: call \`tokrepo_install_plan\` with uuid \`${w.uuid}\` before installing`,
666
- `**Install**: \`tokrepo install ${w.uuid} --target codex --dry-run --json\``,
943
+ `**Install**: \`tokrepo install ${w.uuid} --dry-run --json\``,
667
944
  ``,
668
945
  steps,
669
946
  ].join('\n');
@@ -689,8 +966,8 @@ async function handleInstallPlan(args) {
689
966
  const plan = await fetchInstallPlan(uuid, target);
690
967
  const decision = planPolicyDecision(plan);
691
968
  const command = decision === 'allow'
692
- ? `tokrepo install ${plan.asset_uuid || uuid} --target codex --yes`
693
- : `tokrepo install ${plan.asset_uuid || uuid} --target codex --dry-run --json`;
969
+ ? `tokrepo install ${plan.asset_uuid || uuid} --target ${target} --yes`
970
+ : `tokrepo install ${plan.asset_uuid || uuid} --target ${target} --dry-run --json`;
694
971
  return {
695
972
  content: [{
696
973
  type: 'text',
@@ -1056,7 +1333,18 @@ async function handleRequest(msg) {
1056
1333
  const { name, arguments: args } = params || {};
1057
1334
  let result;
1058
1335
  try {
1336
+ if (!EXPOSED_TOOL_NAMES.has(name)) {
1337
+ return {
1338
+ jsonrpc: '2.0',
1339
+ id,
1340
+ result: {
1341
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
1342
+ isError: true,
1343
+ },
1344
+ };
1345
+ }
1059
1346
  switch (name) {
1347
+ case 'tokrepo_discover': result = await handleDiscover(args || {}); break;
1060
1348
  case 'tokrepo_search': result = await handleSearch(args || {}); break;
1061
1349
  case 'tokrepo_detail': result = await handleDetail(args || {}); break;
1062
1350
  case 'tokrepo_install': result = await handleInstall(args || {}); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo-mcp-server",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
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": {