package.json
json{
"name": "reuseio",
"version": "0.5.1",
"description": "Thin client and MCP server for the Reuseio software registry",
"engines": { "node": ">=20" },
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./mcp": {
"types": "./dist/mcp.d.ts",
"import": "./dist/mcp.js"
}
},
"bin": {
"reuseio": "bin/reuseio.js",
"reuseio-mcp": "bin/reuseio-mcp.js"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"files": [
"bin/reuseio.js",
"dist",
"src",
"README.md"
],
"license": "MIT",
"devDependencies": {
"typescript": "^5.9.2"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.4.3"
}
}
README.md
markdown# reuseio
Thin client and read-only MCP server for reading the Reuseio Registry. It performs no LLM analysis and never executes or proxies products.
Before using the package in a repository, run `npx reuseio sync --check`. If the official release center reports a newer version, run `npx reuseio sync --root .`; it validates the official content, backs up existing Skill/npm distribution files, and then updates the allow-listed files.
```ts
import { createClient } from 'reuseio'
const registry = createClient()
const results = await registry.search('object storage')
const manifest = await registry.getManifest('cloudflare-r2')
const officialReleases = await registry.getOfficialVersions()
// Let your agent gather evidence for a technology-selection task, then make the final choice.
const research = await registry.researchTask({
task: 'Build a file upload service with private object storage and signed downloads',
requiredCapabilities: ['private object storage', 'signed downloads'],
preferredCapabilities: ['Node SDK'],
maxProducts: 20,
})
for (const candidate of research.candidates) {
console.log(candidate.product, candidate.requiredCoverage, candidate.manifest.sources, candidate.implementationPrompt)
}
console.log(research.decisionRationale)
// Simple code changes are skipped locally and do not call the registry.
const skipped = await registry.researchTask({ task: 'Rename a local variable and fix its unit test' })
console.log(skipped.skipped, skipped.skipReason)
// When Reuseio coverage is incomplete, the SDK also returns targeted npm
// keyword and (when enabled) GitHub topic leads. They are explicitly
// unverified and must be checked by the agent.
const expanded = await registry.researchTask({
task: 'Build a private object storage service with signed downloads',
requiredCapabilities: ['object storage', 'signed downloads'],
preferredCapabilities: ['Node SDK'],
fallback: { providers: ['github', 'npm'], maxResultsPerTerm: 5 },
})
console.log(expanded.fallback?.candidates)
// Independent capability searches run in parallel and are deduplicated.
const candidates = await registry.searchMany(['object storage', 'signed downloads'], { maxResults: 8 })
// MCP server (stdio): add the npm package to an MCP host such as Claude Code,
// Cursor, or VS Code. The server is read-only and keeps stdout exclusively for
// MCP JSON-RPC frames.
// {
// "mcpServers": {
// "reuseio": { "command": "npx", "args": ["-y", "--package", "reuseio@latest", "reuseio-mcp"] }
// }
// }
// Or run it directly: npx -y --package reuseio@latest reuseio-mcp
// Compare the local Skill/npm distribution with the official Reuseio release manifest.
// The command only overwrites files when the official version is newer and creates a backup first.
// Run from the repository root:
// npx reuseio sync --root .
// Add --check to inspect versions without changing files.
```
`researchTask` does not call an LLM or execute a provider. It skips simple code changes, then returns ranked Reuseio candidates with required/preferred capability coverage, compact official-source excerpts, evidence, implementation constraints, and a `decisionRationale` chain of `Requirement → Capability → Evidence → Decision`. If coverage is incomplete, it searches npm package keywords and optionally GitHub repository topics, returning explicitly unverified external leads. The calling agent must read official README/docs/package metadata and license information before relying on those leads. Set `force: true` when a task is ambiguous but technology selection is intentional. `getManifest` and `getSources` remain available when the agent needs complete Reuseio evidence.
The package also ships a read-only MCP server using the standard stdio transport (Node.js 20+). It exposes `reuseio_search`, `reuseio_search_many`, `reuseio_research`, `reuseio_get_manifest`, `reuseio_get_sources`, `reuseio_get_ai_prompt`, and `reuseio_get_official_versions`. Configure the host to run `npx -y --package reuseio@latest reuseio-mcp` (or install the package and run `reuseio-mcp`). Set `REUSEIO_API_URL` only when using a compatible Reuseio API base; stdout is reserved for MCP JSON-RPC and diagnostics go to stderr.
GET responses are cached in memory for 30 seconds by default and requests time out after 15 seconds. Override these with `cacheTtlMs` and `timeoutMs` when creating the client.
The official version manifest is available at `https://reuseio.com/api/v1/versions` and the human-readable release page is `https://reuseio.com/versions`. The SDK also exports `compareVersions` and `getSyncDecision` for integrations that need to implement the same version gate.
dist/index.js
javascriptexport { compareVersions, getSyncDecision } from './version-sync.js';
const SELECTION_SIGNALS = ['api', 'sdk', 'database', 'storage', 'queue', 'message broker', 'oauth', 'authentication', 'infrastructure', 'cloud', 'hosting', 'deployment', 'third-party', 'provider', 'integration', 'webhook', '数据库', '存储', '队列', '云服务', '部署', '基础设施', '第三方', '集成', '鉴权', '认证', '接口', '服务商'];
const SIMPLE_CHANGE_SIGNALS = ['fix typo', 'rename variable', 'formatting', 'lint', 'refactor', 'unit test', 'simple bug fix', '修复拼写', '重命名变量', '格式化', '代码重构', '单元测试', '简单修复'];
const uniqueCapabilities = (values = []) => [...new Set(values.map(value => value.trim().replace(/\s+/g, ' ')).filter(value => value.length >= 2 && value.length <= 80))];
const comparable = (value) => value.toLowerCase().replace(/[\s_-]+/g, '');
const matchesCapability = (left, right) => { const a = comparable(left); const b = comparable(right); return Boolean(a && b && (a.includes(b) || b.includes(a))); };
export const shouldTriggerResearch = (task, requiredCapabilities = [], preferredCapabilities = []) => {
if (requiredCapabilities.length || preferredCapabilities.length)
return true;
const normalized = task.toLowerCase();
return !SIMPLE_CHANGE_SIGNALS.some(signal => normalized.includes(signal)) && SELECTION_SIGNALS.some(signal => normalized.includes(signal));
};
export class ReuseioClient {
baseUrl;
request;
timeoutMs;
cacheTtlMs;
cache = new Map();
constructor(options = {}) {
this.baseUrl = (options.baseUrl || 'https://reuseio.com/api/v1').replace(/\/$/, '');
this.request = options.fetch || globalThis.fetch;
this.timeoutMs = Math.max(1_000, options.timeoutMs || 15_000);
this.cacheTtlMs = Math.max(0, options.cacheTtlMs ?? 30_000);
}
async fetchWithTimeout(input, init = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
try {
return await this.request(input, { ...init, signal: init.signal || controller.signal });
}
finally {
clearTimeout(timeout);
}
}
async get(path, query) {
const url = new URL(`${this.baseUrl}${path}`);
for (const [key, value] of Object.entries(query || {}))
if (value !== undefined)
url.searchParams.set(key, String(value));
const cacheKey = url.toString();
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > Date.now())
return cached.value;
const response = await this.fetchWithTimeout(url, { headers: { accept: 'application/json' } });
if (!response.ok)
throw new Error(`Reuseio API ${response.status}: ${(await response.text()).slice(0, 300)}`);
const payload = await response.json();
if (this.cacheTtlMs)
this.cache.set(cacheKey, { expiresAt: Date.now() + this.cacheTtlMs, value: payload.data });
return payload.data;
}
async post(path, body) {
const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(body) });
if (!response.ok)
throw new Error(`Reuseio API ${response.status}: ${(await response.text()).slice(0, 300)}`);
const payload = await response.json();
return payload.data;
}
async externalGet(url, headers = {}) {
const response = await this.fetchWithTimeout(url, { headers: { accept: 'application/json', ...headers } });
if (!response.ok)
throw new Error(`External registry ${response.status}: ${(await response.text()).slice(0, 300)}`);
return response.json();
}
search(query, options = {}) {
return this.get('/search', { q: query, page: options.page, per_page: options.perPage, type: options.type });
}
async searchMany(queries, options = {}) {
const uniqueQueries = [...new Set(queries.map(query => query.trim()).filter(Boolean))].slice(0, 12);
const groups = await Promise.all(uniqueQueries.map(async (query) => ({ query, results: await this.search(query, { perPage: options.perPage || 10, type: options.type }) })));
const merged = new Map();
for (const group of groups)
for (const item of group.results) {
const key = String(item.slug || item.id || item.name);
const existing = merged.get(key) || { ...item, _matchedQueries: [], _score: 0 };
existing._matchedQueries = [...(existing._matchedQueries || []), group.query];
existing._score = (existing._score || 0) + 1;
merged.set(key, existing);
}
return [...merged.values()].sort((left, right) => (right._score || 0) - (left._score || 0)).slice(0, options.maxResults || 20);
}
searchGithub(query, options = {}) {
const limit = Math.max(1, Math.min(options.maxResults || 8, 20));
const headers = options.token ? { authorization: `Bearer ${options.token}` } : {};
const searchMode = options.searchMode || 'topic';
const searchQuery = searchMode === 'topic'
? `topic:${query.trim().toLowerCase().replace(/[\s_]+/g, '-')}`
: `${query} in:name,description,readme`;
return this.externalGet(`https://api.github.com/search/repositories?q=${encodeURIComponent(searchQuery)}&sort=stars&order=desc&per_page=${limit}`, headers).then(result => result.items.map(item => ({ provider: 'github', name: item.full_name, description: item.description || null, url: item.html_url, documentationUrl: item.homepage || null, repositoryUrl: item.html_url, verified: false, stars: item.stargazers_count, forks: item.forks_count, language: item.language || null, keywords: item.topics || [] })));
}
searchNpm(query, options = {}) {
const limit = Math.max(1, Math.min(options.maxResults || 8, 20));
const registryUrl = (options.registryUrl || 'https://registry.npmjs.org').replace(/\/$/, '');
const queryMode = options.queryMode || 'keywords';
const searchText = queryMode === 'keywords' ? `keywords:${query.trim()}` : query;
return this.externalGet(`${registryUrl}/-/v1/search?text=${encodeURIComponent(searchText)}&size=${limit}`).then(result => result.objects.map(item => ({ provider: 'npm', name: item.package.name, description: item.package.description || null, url: item.package.links?.npm || `${registryUrl}/package/${item.package.name}`, documentationUrl: item.package.links?.homepage || item.package.links?.npm || null, repositoryUrl: typeof item.package.links?.repository === 'string' ? item.package.links.repository : item.package.links?.repository?.url || null, verified: false, version: item.package.version || null, weeklyDownloads: item.downloads?.monthly, keywords: item.package.keywords || [] })));
}
async externalFallback(terms, options) {
const calls = [];
for (const term of terms) {
if (options.providers.includes('github'))
calls.push(this.searchGithub(term, { maxResults: options.maxResultsPerTerm, token: options.githubToken, searchMode: 'topic' }));
if (options.providers.includes('npm'))
calls.push(this.searchNpm(term, { maxResults: options.maxResultsPerTerm, registryUrl: options.npmRegistryUrl, queryMode: 'keywords' }));
}
const groups = await Promise.allSettled(calls);
const seen = new Set();
const candidates = [];
for (const group of groups)
if (group.status === 'fulfilled')
for (const candidate of group.value) {
const key = `${candidate.provider}:${candidate.name}`;
if (!seen.has(key)) {
seen.add(key);
candidates.push(candidate);
}
}
return candidates;
}
getProduct(slug) { return this.get(`/products/${encodeURIComponent(slug)}`); }
getCapability(slug) { return this.get(`/capabilities/${encodeURIComponent(slug)}`); }
getManifest(slug) { return this.get(`/products/${encodeURIComponent(slug)}/manifest`); }
getSources(slug) { return this.get(`/products/${encodeURIComponent(slug)}/sources`); }
getAiPrompt(slug) { return this.get(`/products/${encodeURIComponent(slug)}/ai-prompt`); }
getOfficialVersions() { return this.get('/versions'); }
async researchTask(request, externalOptions = {}) {
const requiredCapabilities = uniqueCapabilities([...(request.capabilities || []), ...(request.requiredCapabilities || [])]);
const preferredCapabilities = uniqueCapabilities(request.preferredCapabilities || []).filter(capability => !requiredCapabilities.includes(capability));
if (!request.force && !shouldTriggerResearch(request.task, requiredCapabilities, preferredCapabilities)) {
return { task: request.task, capabilities: [], requiredCapabilities, preferredCapabilities, candidates: [], skipped: true, skipReason: 'No API, SDK, infrastructure, or third-party technology selection is present in this task.', guidance: ['Skip Reuseio for simple code edits, formatting, tests, or isolated bug fixes.', 'Invoke Reuseio when the task requires choosing an API, SDK, infrastructure component, database, cloud service, or third-party provider.'], decisionRationale: { requirements: { required: requiredCapabilities, preferred: preferredCapabilities }, capabilityMatches: [], evidence: [], decision: { selectedCandidate: null, reason: 'Research skipped because the task does not require a technology selection.', rejectedAlternatives: [] } } };
}
const primary = await this.post('/research', { task: request.task, capabilities: request.capabilities, required_capabilities: request.requiredCapabilities, preferred_capabilities: request.preferredCapabilities, force: request.force, max_products: request.maxProducts || 20 });
if (primary.skipped)
return { ...primary, fallback: { triggered: false, terms: [], candidates: [], warning: 'Research was skipped by the decision protocol; no external fallback was performed.' } };
const fallbackConfig = request.fallback === false ? { enabled: false, providers: [], maxResultsPerTerm: 6 } : typeof request.fallback === 'object' ? { enabled: true, providers: request.fallback.providers || ['npm', 'github'], maxResultsPerTerm: Math.min(request.fallback.maxResultsPerTerm || 6, 12) } : { enabled: true, providers: ['npm', 'github'], maxResultsPerTerm: 6 };
// Only canonical capability matches count as coverage. Generic text terms
// are recall hints and must not suppress the external fallback.
const covered = new Set(primary.candidates.flatMap(candidate => (candidate.matchedCapabilities || []).map(value => value.toLowerCase())));
const requestedCapabilities = uniqueCapabilities([...(primary.requiredCapabilities || requiredCapabilities), ...(primary.preferredCapabilities || preferredCapabilities)]);
const terms = requestedCapabilities.filter(term => ![...covered].some(value => matchesCapability(value, term)));
const shouldFallback = fallbackConfig.enabled && (primary.candidates.length === 0 || terms.length > 0);
const fallbackCandidates = shouldFallback ? await this.externalFallback(terms.length ? terms : primary.capabilities, { providers: fallbackConfig.providers, maxResultsPerTerm: fallbackConfig.maxResultsPerTerm, ...externalOptions }) : [];
return { ...primary, fallback: { triggered: shouldFallback, terms: terms.length ? terms : primary.capabilities, candidates: fallbackCandidates, warning: 'External candidates are discovery leads, not Reuseio-verified products. Read their official README, documentation, package metadata, and license before relying on them.' } };
}
}
export const createClient = (options) => new ReuseioClient(options);
dist/index.d.ts
typescriptexport interface ReuseioClientOptions {
baseUrl?: string;
fetch?: typeof globalThis.fetch;
timeoutMs?: number;
cacheTtlMs?: number;
}
export interface ReuseioResponse<T> {
data: T;
meta: Record<string, unknown>;
}
export type FallbackProvider = 'github' | 'npm';
export interface ExternalCandidate {
provider: FallbackProvider;
name: string;
description: string | null;
url: string;
documentationUrl: string | null;
repositoryUrl: string | null;
verified: false;
stars?: number;
forks?: number;
language?: string | null;
version?: string | null;
weeklyDownloads?: number;
keywords?: string[];
}
export interface DecisionRationale {
requirements: {
required: string[];
preferred: string[];
};
capabilityMatches: Array<{
capability: string;
priority: 'required' | 'preferred';
matchedCandidates: string[];
}>;
evidence: Array<{
candidate: string;
product: string;
officialPrimarySourceCount: number;
officialSourceUrls: string[];
}>;
decision: {
selectedCandidate: string | null;
reason: string;
rejectedAlternatives: Array<{
candidate: string;
reason: string;
}>;
};
}
export interface QueryAlignment {
requested: Array<{
input: string;
priority: 'required' | 'preferred';
}>;
resolved: Array<{
input: string;
kind: 'capability' | 'tag';
id: string;
slug: string;
match: 'canonical' | 'alias';
confidence: number;
priority: 'required' | 'preferred';
}>;
unresolved: Array<{
input: string;
priority: 'required' | 'preferred';
}>;
warnings: string[];
}
export interface OfficialReleaseAsset {
id: string;
kind: 'skill' | 'npm';
name: string;
version: string;
contentUrl?: string;
packageUrl?: string;
registryTarballUrl?: string;
coreContent: string[];
coreContentZh?: string[];
}
export interface OfficialReleaseManifest {
schema: string;
manifestVersion: string;
officialUrl: string;
generatedAt?: string;
versionPolicy: {
comparison: string;
trigger: string;
apply: string;
rollback: string;
};
assets: OfficialReleaseAsset[];
}
export interface ResearchRequest {
task: string;
capabilities?: string[];
requiredCapabilities?: string[];
preferredCapabilities?: string[];
maxProducts?: number;
force?: boolean;
fallback?: boolean | {
providers?: FallbackProvider[];
maxResultsPerTerm?: number;
};
}
export interface ResearchCandidate {
product: Record<string, unknown>;
matchedTerms: string[];
matchedCapabilities: string[];
matchedTags: string[];
matchedRequiredCapabilities: string[];
matchedPreferredCapabilities: string[];
requiredCoverage: number;
preferredCoverage: number;
relevanceScore: number;
manifest: Record<string, unknown>;
implementationPrompt: string;
evidence: {
officialPrimarySourceCount: number;
officialSourceUrls: string[];
};
}
export interface ResearchResult {
task: string;
capabilities: string[];
requiredCapabilities: string[];
preferredCapabilities: string[];
candidates: ResearchCandidate[];
guidance: string[];
queryAlignment?: QueryAlignment;
skipped?: boolean;
skipReason?: string;
decisionRationale: DecisionRationale;
fallback?: {
triggered: boolean;
terms: string[];
candidates: ExternalCandidate[];
warning: string;
};
}
export { compareVersions, getSyncDecision } from './version-sync.js';
export declare const shouldTriggerResearch: (task: string, requiredCapabilities?: string[], preferredCapabilities?: string[]) => boolean;
export declare class ReuseioClient {
private readonly baseUrl;
private readonly request;
private readonly timeoutMs;
private readonly cacheTtlMs;
private readonly cache;
constructor(options?: ReuseioClientOptions);
private fetchWithTimeout;
private get;
private post;
private externalGet;
search(query: string, options?: {
page?: number;
perPage?: number;
type?: string;
}): Promise<Record<string, unknown>[]>;
searchMany(queries: string[], options?: {
perPage?: number;
type?: string;
maxResults?: number;
}): Promise<(Record<string, unknown> & {
_matchedQueries?: string[];
_score?: number;
})[]>;
searchGithub(query: string, options?: {
maxResults?: number;
token?: string;
searchMode?: 'topic' | 'text';
}): Promise<{
provider: "github";
name: any;
description: any;
url: any;
documentationUrl: any;
repositoryUrl: any;
verified: false;
stars: any;
forks: any;
language: any;
keywords: any;
}[]>;
searchNpm(query: string, options?: {
maxResults?: number;
registryUrl?: string;
queryMode?: 'keywords' | 'text';
}): Promise<{
provider: "npm";
name: any;
description: any;
url: any;
documentationUrl: any;
repositoryUrl: any;
verified: false;
version: any;
weeklyDownloads: number | undefined;
keywords: any;
}[]>;
private externalFallback;
getProduct(slug: string): Promise<Record<string, unknown>>;
getCapability(slug: string): Promise<Record<string, unknown>>;
getManifest(slug: string): Promise<Record<string, unknown>>;
getSources(slug: string): Promise<Record<string, unknown>[]>;
getAiPrompt(slug: string): Promise<{
product: string;
prompt: string;
}>;
getOfficialVersions(): Promise<OfficialReleaseManifest>;
researchTask(request: ResearchRequest, externalOptions?: {
githubToken?: string;
npmRegistryUrl?: string;
}): Promise<ResearchResult>;
}
export declare const createClient: (options?: ReuseioClientOptions) => ReuseioClient;
dist/version-sync.js
javascriptconst parseVersion = (value) => {
const match = value.trim().replace(/^v/i, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
if (!match)
return null;
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] ? match[4].split('.') : [] };
};
const comparePrerelease = (left, right) => {
if (!left.length && !right.length)
return 0;
if (!left.length)
return 1;
if (!right.length)
return -1;
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1) {
const a = left[index];
const b = right[index];
if (a === undefined)
return -1;
if (b === undefined)
return 1;
if (a === b)
continue;
const aNumber = /^\d+$/.test(a);
const bNumber = /^\d+$/.test(b);
if (aNumber && bNumber)
return Number(a) - Number(b);
if (aNumber !== bNumber)
return aNumber ? -1 : 1;
return a < b ? -1 : 1;
}
return 0;
};
export const compareVersions = (left, right) => {
const a = parseVersion(left);
const b = parseVersion(right);
if (!a || !b)
return Number.NaN;
return a.major - b.major || a.minor - b.minor || a.patch - b.patch || comparePrerelease(a.prerelease, b.prerelease);
};
export const getSyncDecision = (localVersion, remoteVersion) => {
const comparison = compareVersions(localVersion, remoteVersion);
if (Number.isNaN(comparison))
return 'invalid';
if (comparison === 0)
return 'up_to_date';
return comparison < 0 ? 'remote_newer' : 'local_newer';
};
dist/version-sync.d.ts
typescriptexport type SyncDecision = 'up_to_date' | 'remote_newer' | 'local_newer' | 'invalid';
export declare const compareVersions: (left: string, right: string) => number;
export declare const getSyncDecision: (localVersion: string, remoteVersion: string) => SyncDecision;
bin/reuseio.js
javascript#!/usr/bin/env node
import { execFile as execFileCallback } from 'node:child_process'
import { promisify } from 'node:util'
import { existsSync } from 'node:fs'
import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
import { getSyncDecision } from '../dist/version-sync.js'
const execFile = promisify(execFileCallback)
const DEFAULT_MANIFEST_URL = 'https://reuseio.com/api/v1/versions'
const MAX_TEXT_BYTES = 1_000_000
const MAX_TARBALL_BYTES = 20_000_000
const ALLOWED_SKILL_TARGETS = new Set(['skill/reuseio-registry/SKILL.md', 'public/skill/reuseio-registry/SKILL.md'])
const ALLOWED_NPM_TARGETS = new Set([
'sdk/package.json', 'sdk/README.md', 'sdk/dist/index.js', 'sdk/dist/index.d.ts',
'sdk/dist/version-sync.js', 'sdk/dist/version-sync.d.ts', 'sdk/bin/reuseio.js',
'sdk/bin/reuseio-mcp.js', 'sdk/src/index.ts', 'sdk/src/version-sync.ts',
'sdk/src/mcp.ts', 'sdk/dist/mcp.js', 'sdk/dist/mcp.d.ts',
])
const ALLOWED_NPM_SOURCES = new Set(['package.json', 'README.md', 'dist/index.js', 'dist/index.d.ts', 'dist/version-sync.js', 'dist/version-sync.d.ts', 'bin/reuseio.js', 'bin/reuseio-mcp.js', 'src/index.ts', 'src/version-sync.ts', 'src/mcp.ts', 'dist/mcp.js', 'dist/mcp.d.ts'])
const parseArgs = (argv) => {
const flags = { command: argv[0] && !argv[0].startsWith('-') ? argv[0] : 'sync', root: process.cwd(), manifestUrl: process.env.REUSEIO_OFFICIAL_MANIFEST_URL || DEFAULT_MANIFEST_URL, dryRun: false, check: false }
if (flags.command === 'status') flags.check = true
for (let index = flags.command === argv[0] ? 1 : 0; index < argv.length; index += 1) {
const value = argv[index]
if (value === '--dry-run') flags.dryRun = true
else if (value === '--check' || value === 'status') flags.check = true
else if (value === '--root') flags.root = argv[++index] || flags.root
else if (value === '--manifest-url') flags.manifestUrl = argv[++index] || flags.manifestUrl
}
return flags
}
const readJson = async (file) => JSON.parse(await readFile(file, 'utf8'))
const fetchText = async (url) => {
const response = await fetch(url, { headers: { accept: 'text/plain, application/json' } })
if (!response.ok) throw new Error(`Official source ${response.status}: ${url}`)
const contentLength = Number(response.headers.get('content-length') || 0)
if (contentLength > MAX_TEXT_BYTES) throw new Error(`Official source is larger than ${MAX_TEXT_BYTES} bytes: ${url}`)
const text = await response.text()
if (Buffer.byteLength(text) > MAX_TEXT_BYTES) throw new Error(`Official source is larger than ${MAX_TEXT_BYTES} bytes: ${url}`)
return text
}
const fetchManifest = async (url) => {
const payload = JSON.parse(await fetchText(url))
const manifest = payload?.data?.assets ? payload.data : payload
if (!Array.isArray(manifest?.assets) || !manifest.assets.length) throw new Error('Official version manifest has no assets')
return manifest
}
const assertSafeRelativePath = (root, target) => {
if (!target || isAbsolute(target)) throw new Error(`Unsafe sync target: ${target}`)
const resolvedRoot = resolve(root)
const resolvedTarget = resolve(root, target)
if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${sep}`)) throw new Error(`Sync target escapes root: ${target}`)
return resolvedTarget
}
const assertOfficialUrl = (url, manifestUrl, kind) => {
const value = new URL(url)
const manifestHost = new URL(manifestUrl).hostname
const officialHosts = new Set([manifestHost, 'reuseio.com', 'www.reuseio.com'])
const allowed = value.protocol === 'https:' && (officialHosts.has(value.hostname) || (kind === 'npm' && value.hostname === 'registry.npmjs.org'))
if (!allowed) throw new Error(`Refusing non-official ${kind} source: ${url}`)
}
const versionFromSkill = (content) => content.match(/^version:\s*['"]?([^'"\s]+)['"]?\s*$/m)?.[1] || '0.0.0'
const localVersionForAsset = async (root, asset) => {
if (asset.kind === 'skill') {
for (const target of asset.localTargets || []) {
const file = assertSafeRelativePath(root, target)
if (existsSync(file)) return versionFromSkill(await readFile(file, 'utf8'))
}
return '0.0.0'
}
const packageTarget = (asset.localTargets || []).find(target => target.endsWith('/package.json'))
if (!packageTarget) return '0.0.0'
const file = assertSafeRelativePath(root, packageTarget)
if (!existsSync(file)) return '0.0.0'
return String((await readJson(file)).version || '0.0.0')
}
const timestamp = () => new Date().toISOString().replace(/[:.]/g, '-')
const backupFile = async (root, file, backupRoot) => {
if (!existsSync(file)) return
const backup = join(backupRoot, relative(root, file))
await mkdir(dirname(backup), { recursive: true })
await copyFile(file, backup)
}
const writeAtomic = async (file, content) => {
await mkdir(dirname(file), { recursive: true })
const temporary = `${file}.reuseio-${process.pid}-${Date.now()}.tmp`
await writeFile(temporary, content)
await rename(temporary, file)
}
const syncSkill = async (root, asset, backupRoot, manifestUrl, dryRun) => {
const sourceUrl = new URL(asset.contentPath, manifestUrl).toString()
assertOfficialUrl(sourceUrl, manifestUrl, asset.kind)
const content = await fetchText(sourceUrl)
if (!content.startsWith('---') || versionFromSkill(content) !== asset.version) throw new Error(`Skill content version does not match manifest ${asset.version}`)
for (const target of asset.localTargets || []) {
if (!ALLOWED_SKILL_TARGETS.has(target)) throw new Error(`Refusing unallow-listed Skill target: ${target}`)
const file = assertSafeRelativePath(root, target)
if (dryRun) { console.log(`would update ${relative(root, file)}`); continue }
await backupFile(root, file, backupRoot)
await writeAtomic(file, content)
}
}
const safeTarEntry = (entry) => {
const normalized = entry.replaceAll('\\', '/')
return normalized.startsWith('package/') && !normalized.includes('/../') && !normalized.includes('/./') && !normalized.endsWith('/')
}
const syncNpm = async (root, asset, backupRoot, manifestUrl, dryRun) => {
if (asset.packageName !== 'reuseio') throw new Error(`Refusing unexpected npm package: ${asset.packageName}`)
assertOfficialUrl(asset.registryTarballUrl, manifestUrl, asset.kind)
const workspace = await mkdtemp(join(tmpdir(), 'reuseio-sync-'))
try {
const response = await fetch(asset.registryTarballUrl, { headers: { accept: 'application/octet-stream' } })
if (!response.ok) throw new Error(`Official npm source ${response.status}: ${asset.registryTarballUrl}`)
const contentLength = Number(response.headers.get('content-length') || 0)
if (contentLength > MAX_TARBALL_BYTES) throw new Error(`Official npm tarball is larger than ${MAX_TARBALL_BYTES} bytes`)
const tarball = join(workspace, `${asset.packageName}-${asset.version}.tgz`)
const bytes = Buffer.from(await response.arrayBuffer())
if (bytes.byteLength > MAX_TARBALL_BYTES) throw new Error(`Official npm tarball is larger than ${MAX_TARBALL_BYTES} bytes`)
await writeFile(tarball, bytes)
const listing = (await execFile('tar', ['-tzf', tarball], { maxBuffer: 2_000_000 })).stdout.split('\n').map(line => line.trim()).filter(Boolean)
if (listing.some(entry => !safeTarEntry(entry))) throw new Error('Refusing npm tarball with unsafe paths')
const extractRoot = join(workspace, 'package')
await mkdir(extractRoot, { recursive: true })
await execFile('tar', ['-xzf', tarball, '-C', workspace], { maxBuffer: 2_000_000 })
const extractedPackage = await readJson(join(extractRoot, 'package.json'))
if (String(extractedPackage.version) !== asset.version) throw new Error(`npm package version ${extractedPackage.version} does not match manifest ${asset.version}`)
for (const mapping of asset.contentFiles || []) {
if (!mapping || !ALLOWED_NPM_SOURCES.has(mapping.source) || !ALLOWED_NPM_TARGETS.has(mapping.target)) throw new Error(`Refusing unallow-listed npm mapping: ${mapping?.source} -> ${mapping?.target}`)
const target = assertSafeRelativePath(root, mapping.target)
const source = join(extractRoot, mapping.source)
if (!existsSync(source)) throw new Error(`Official npm package is missing ${mapping.source}`)
if (dryRun) { console.log(`would update ${relative(root, target)}`); continue }
await backupFile(root, target, backupRoot)
await mkdir(dirname(target), { recursive: true })
await copyFile(source, target)
}
} finally {
await rm(workspace, { recursive: true, force: true })
}
}
const main = async () => {
const flags = parseArgs(process.argv.slice(2))
const root = resolve(flags.root)
const manifest = await fetchManifest(flags.manifestUrl)
const statuses = []
const backupRoot = join(root, '.reuseio-backups', timestamp())
for (const asset of manifest.assets) {
if (!asset.id || !asset.kind || !asset.version) throw new Error('Official version manifest contains an invalid asset')
const localVersion = await localVersionForAsset(root, asset)
const decision = getSyncDecision(localVersion, asset.version)
statuses.push({ id: asset.id, kind: asset.kind, localVersion, remoteVersion: asset.version, decision })
if (flags.check || decision !== 'remote_newer') continue
if (asset.kind === 'skill') await syncSkill(root, asset, backupRoot, flags.manifestUrl, flags.dryRun)
else if (asset.kind === 'npm') await syncNpm(root, asset, backupRoot, flags.manifestUrl, flags.dryRun)
else throw new Error(`Unsupported official asset kind: ${asset.kind}`)
}
console.log(JSON.stringify({ manifestUrl: flags.manifestUrl, statuses, backupRoot: flags.check || flags.dryRun ? null : backupRoot }, null, 2))
}
if (process.argv[1] && globalThis._importMeta_.url === pathToFileURL(process.argv[1]).href) main().catch(error => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1 })
export { getSyncDecision, parseArgs }
bin/reuseio-mcp.js
javascript#!/usr/bin/env node
import { runMcpServer } from '../dist/mcp.js'
runMcpServer().catch((error) => {
// stdout is reserved for MCP JSON-RPC frames; diagnostics belong on stderr.
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})
src/index.ts
typescriptexport interface ReuseioClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; timeoutMs?: number; cacheTtlMs?: number }
export interface ReuseioResponse<T> { data: T; meta: Record<string, unknown> }
export type FallbackProvider = 'github' | 'npm'
export interface ExternalCandidate { provider: FallbackProvider; name: string; description: string | null; url: string; documentationUrl: string | null; repositoryUrl: string | null; verified: false; stars?: number; forks?: number; language?: string | null; version?: string | null; weeklyDownloads?: number; keywords?: string[] }
export interface DecisionRationale { requirements: { required: string[]; preferred: string[] }; capabilityMatches: Array<{ capability: string; priority: 'required' | 'preferred'; matchedCandidates: string[] }>; evidence: Array<{ candidate: string; product: string; officialPrimarySourceCount: number; officialSourceUrls: string[] }>; decision: { selectedCandidate: string | null; reason: string; rejectedAlternatives: Array<{ candidate: string; reason: string }> } }
export interface QueryAlignment { requested: Array<{ input: string; priority: 'required' | 'preferred' }>; resolved: Array<{ input: string; kind: 'capability' | 'tag'; id: string; slug: string; match: 'canonical' | 'alias'; confidence: number; priority: 'required' | 'preferred' }>; unresolved: Array<{ input: string; priority: 'required' | 'preferred' }>; warnings: string[] }
export interface OfficialReleaseAsset { id: string; kind: 'skill' | 'npm'; name: string; version: string; contentUrl?: string; packageUrl?: string; registryTarballUrl?: string; coreContent: string[]; coreContentZh?: string[] }
export interface OfficialReleaseManifest { schema: string; manifestVersion: string; officialUrl: string; generatedAt?: string; versionPolicy: { comparison: string; trigger: string; apply: string; rollback: string }; assets: OfficialReleaseAsset[] }
export interface ResearchRequest { task: string; capabilities?: string[]; requiredCapabilities?: string[]; preferredCapabilities?: string[]; maxProducts?: number; force?: boolean; fallback?: boolean | { providers?: FallbackProvider[]; maxResultsPerTerm?: number } }
export interface ResearchCandidate { product: Record<string, unknown>; matchedTerms: string[]; matchedCapabilities: string[]; matchedTags: string[]; matchedRequiredCapabilities: string[]; matchedPreferredCapabilities: string[]; requiredCoverage: number; preferredCoverage: number; relevanceScore: number; manifest: Record<string, unknown>; implementationPrompt: string; evidence: { officialPrimarySourceCount: number; officialSourceUrls: string[] } }
export interface ResearchResult { task: string; capabilities: string[]; requiredCapabilities: string[]; preferredCapabilities: string[]; candidates: ResearchCandidate[]; guidance: string[]; queryAlignment?: QueryAlignment; skipped?: boolean; skipReason?: string; decisionRationale: DecisionRationale; fallback?: { triggered: boolean; terms: string[]; candidates: ExternalCandidate[]; warning: string } }
export { compareVersions, getSyncDecision } from './version-sync.js'
const SELECTION_SIGNALS = ['api', 'sdk', 'database', 'storage', 'queue', 'message broker', 'oauth', 'authentication', 'infrastructure', 'cloud', 'hosting', 'deployment', 'third-party', 'provider', 'integration', 'webhook', '数据库', '存储', '队列', '云服务', '部署', '基础设施', '第三方', '集成', '鉴权', '认证', '接口', '服务商']
const SIMPLE_CHANGE_SIGNALS = ['fix typo', 'rename variable', 'formatting', 'lint', 'refactor', 'unit test', 'simple bug fix', '修复拼写', '重命名变量', '格式化', '代码重构', '单元测试', '简单修复']
const uniqueCapabilities = (values: string[] = []) => [...new Set(values.map(value => value.trim().replace(/\s+/g, ' ')).filter(value => value.length >= 2 && value.length <= 80))]
const comparable = (value: string) => value.toLowerCase().replace(/[\s_-]+/g, '')
const matchesCapability = (left: string, right: string) => { const a = comparable(left); const b = comparable(right); return Boolean(a && b && (a.includes(b) || b.includes(a))) }
export const shouldTriggerResearch = (task: string, requiredCapabilities: string[] = [], preferredCapabilities: string[] = []) => {
if (requiredCapabilities.length || preferredCapabilities.length) return true
const normalized = task.toLowerCase()
return !SIMPLE_CHANGE_SIGNALS.some(signal => normalized.includes(signal)) && SELECTION_SIGNALS.some(signal => normalized.includes(signal))
}
export class ReuseioClient {
private readonly baseUrl: string
private readonly request: typeof globalThis.fetch
private readonly timeoutMs: number
private readonly cacheTtlMs: number
private readonly cache = new Map<string, { expiresAt: number; value: unknown }>()
constructor(options: ReuseioClientOptions = {}) {
this.baseUrl = (options.baseUrl || 'https://reuseio.com/api/v1').replace(/\/$/, '')
this.request = options.fetch || globalThis.fetch
this.timeoutMs = Math.max(1_000, options.timeoutMs || 15_000)
this.cacheTtlMs = Math.max(0, options.cacheTtlMs ?? 30_000)
}
private async fetchWithTimeout(input: string | URL, init: RequestInit = {}) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), this.timeoutMs)
try { return await this.request(input, { ...init, signal: init.signal || controller.signal }) } finally { clearTimeout(timeout) }
}
private async get<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`)
for (const [key, value] of Object.entries(query || {})) if (value !== undefined) url.searchParams.set(key, String(value))
const cacheKey = url.toString()
const cached = this.cache.get(cacheKey)
if (cached && cached.expiresAt > Date.now()) return cached.value as T
const response = await this.fetchWithTimeout(url, { headers: { accept: 'application/json' } })
if (!response.ok) throw new Error(`Reuseio API ${response.status}: ${(await response.text()).slice(0, 300)}`)
const payload = await response.json() as ReuseioResponse<T>
if (this.cacheTtlMs) this.cache.set(cacheKey, { expiresAt: Date.now() + this.cacheTtlMs, value: payload.data })
return payload.data
}
private async post<T>(path: string, body: unknown): Promise<T> {
const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(body) })
if (!response.ok) throw new Error(`Reuseio API ${response.status}: ${(await response.text()).slice(0, 300)}`)
const payload = await response.json() as ReuseioResponse<T>
return payload.data
}
private async externalGet<T>(url: string, headers: Record<string, string> = {}) {
const response = await this.fetchWithTimeout(url, { headers: { accept: 'application/json', ...headers } })
if (!response.ok) throw new Error(`External registry ${response.status}: ${(await response.text()).slice(0, 300)}`)
return response.json() as Promise<T>
}
search(query: string, options: { page?: number; perPage?: number; type?: string } = {}) {
return this.get<Array<Record<string, unknown>>>('/search', { q: query, page: options.page, per_page: options.perPage, type: options.type })
}
async searchMany(queries: string[], options: { perPage?: number; type?: string; maxResults?: number } = {}) {
const uniqueQueries = [...new Set(queries.map(query => query.trim()).filter(Boolean))].slice(0, 12)
const groups = await Promise.all(uniqueQueries.map(async query => ({ query, results: await this.search(query, { perPage: options.perPage || 10, type: options.type }) })))
const merged = new Map<string, Record<string, unknown> & { _matchedQueries?: string[]; _score?: number }>()
for (const group of groups) for (const item of group.results) {
const key = String(item.slug || item.id || item.name)
const existing = merged.get(key) || { ...item, _matchedQueries: [], _score: 0 }
existing._matchedQueries = [...(existing._matchedQueries || []), group.query]
existing._score = (existing._score || 0) + 1
merged.set(key, existing)
}
return [...merged.values()].sort((left, right) => (right._score || 0) - (left._score || 0)).slice(0, options.maxResults || 20)
}
searchGithub(query: string, options: { maxResults?: number; token?: string; searchMode?: 'topic' | 'text' } = {}) {
const limit = Math.max(1, Math.min(options.maxResults || 8, 20))
const headers: Record<string, string> = options.token ? { authorization: `Bearer ${options.token}` } : {}
const searchMode = options.searchMode || 'topic'
const searchQuery = searchMode === 'topic'
? `topic:${query.trim().toLowerCase().replace(/[\s_]+/g, '-')}`
: `${query} in:name,description,readme`
return this.externalGet<{ items: Array<Record<string, any>> }>(`https://api.github.com/search/repositories?q=${encodeURIComponent(searchQuery)}&sort=stars&order=desc&per_page=${limit}`, headers).then(result => result.items.map(item => ({ provider: 'github' as const, name: item.full_name, description: item.description || null, url: item.html_url, documentationUrl: item.homepage || null, repositoryUrl: item.html_url, verified: false as const, stars: item.stargazers_count, forks: item.forks_count, language: item.language || null, keywords: item.topics || [] })))
}
searchNpm(query: string, options: { maxResults?: number; registryUrl?: string; queryMode?: 'keywords' | 'text' } = {}) {
const limit = Math.max(1, Math.min(options.maxResults || 8, 20)); const registryUrl = (options.registryUrl || 'https://registry.npmjs.org').replace(/\/$/, '')
const queryMode = options.queryMode || 'keywords'
const searchText = queryMode === 'keywords' ? `keywords:${query.trim()}` : query
return this.externalGet<{ objects: Array<{ package: Record<string, any>; downloads?: { monthly?: number } }> }>(`${registryUrl}/-/v1/search?text=${encodeURIComponent(searchText)}&size=${limit}`).then(result => result.objects.map(item => ({ provider: 'npm' as const, name: item.package.name, description: item.package.description || null, url: item.package.links?.npm || `${registryUrl}/package/${item.package.name}`, documentationUrl: item.package.links?.homepage || item.package.links?.npm || null, repositoryUrl: typeof item.package.links?.repository === 'string' ? item.package.links.repository : item.package.links?.repository?.url || null, verified: false as const, version: item.package.version || null, weeklyDownloads: item.downloads?.monthly, keywords: item.package.keywords || [] })))
}
private async externalFallback(terms: string[], options: { providers: FallbackProvider[]; maxResultsPerTerm: number; githubToken?: string; npmRegistryUrl?: string }) {
const calls: Array<Promise<ExternalCandidate[]>> = []
for (const term of terms) {
if (options.providers.includes('github')) calls.push(this.searchGithub(term, { maxResults: options.maxResultsPerTerm, token: options.githubToken, searchMode: 'topic' }))
if (options.providers.includes('npm')) calls.push(this.searchNpm(term, { maxResults: options.maxResultsPerTerm, registryUrl: options.npmRegistryUrl, queryMode: 'keywords' }))
}
const groups = await Promise.allSettled(calls)
const seen = new Set<string>(); const candidates: ExternalCandidate[] = []
for (const group of groups) if (group.status === 'fulfilled') for (const candidate of group.value) {
const key = `${candidate.provider}:${candidate.name}`
if (!seen.has(key)) { seen.add(key); candidates.push(candidate) }
}
return candidates
}
getProduct(slug: string) { return this.get<Record<string, unknown>>(`/products/${encodeURIComponent(slug)}`) }
getCapability(slug: string) { return this.get<Record<string, unknown>>(`/capabilities/${encodeURIComponent(slug)}`) }
getManifest(slug: string) { return this.get<Record<string, unknown>>(`/products/${encodeURIComponent(slug)}/manifest`) }
getSources(slug: string) { return this.get<Array<Record<string, unknown>>>(`/products/${encodeURIComponent(slug)}/sources`) }
getAiPrompt(slug: string) { return this.get<{ product: string; prompt: string }>(`/products/${encodeURIComponent(slug)}/ai-prompt`) }
getOfficialVersions() { return this.get<OfficialReleaseManifest>('/versions') }
async researchTask(request: ResearchRequest, externalOptions: { githubToken?: string; npmRegistryUrl?: string } = {}): Promise<ResearchResult> {
const requiredCapabilities = uniqueCapabilities([...(request.capabilities || []), ...(request.requiredCapabilities || [])])
const preferredCapabilities = uniqueCapabilities(request.preferredCapabilities || []).filter(capability => !requiredCapabilities.includes(capability))
if (!request.force && !shouldTriggerResearch(request.task, requiredCapabilities, preferredCapabilities)) {
return { task: request.task, capabilities: [], requiredCapabilities, preferredCapabilities, candidates: [], skipped: true, skipReason: 'No API, SDK, infrastructure, or third-party technology selection is present in this task.', guidance: ['Skip Reuseio for simple code edits, formatting, tests, or isolated bug fixes.', 'Invoke Reuseio when the task requires choosing an API, SDK, infrastructure component, database, cloud service, or third-party provider.'], decisionRationale: { requirements: { required: requiredCapabilities, preferred: preferredCapabilities }, capabilityMatches: [], evidence: [], decision: { selectedCandidate: null, reason: 'Research skipped because the task does not require a technology selection.', rejectedAlternatives: [] } } } satisfies ResearchResult
}
const primary = await this.post<ResearchResult>('/research', { task: request.task, capabilities: request.capabilities, required_capabilities: request.requiredCapabilities, preferred_capabilities: request.preferredCapabilities, force: request.force, max_products: request.maxProducts || 20 })
if (primary.skipped) return { ...primary, fallback: { triggered: false, terms: [], candidates: [], warning: 'Research was skipped by the decision protocol; no external fallback was performed.' } }
const fallbackConfig = request.fallback === false ? { enabled: false, providers: [] as FallbackProvider[], maxResultsPerTerm: 6 } : typeof request.fallback === 'object' ? { enabled: true, providers: request.fallback.providers || ['npm', 'github'], maxResultsPerTerm: Math.min(request.fallback.maxResultsPerTerm || 6, 12) } : { enabled: true, providers: ['npm', 'github'] as FallbackProvider[], maxResultsPerTerm: 6 }
// Only canonical capability matches count as coverage. Generic text terms
// are recall hints and must not suppress the external fallback.
const covered = new Set(primary.candidates.flatMap(candidate => (candidate.matchedCapabilities || []).map(value => value.toLowerCase())))
const requestedCapabilities = uniqueCapabilities([...(primary.requiredCapabilities || requiredCapabilities), ...(primary.preferredCapabilities || preferredCapabilities)])
const terms = requestedCapabilities.filter(term => ![...covered].some(value => matchesCapability(value, term)))
const shouldFallback = fallbackConfig.enabled && (primary.candidates.length === 0 || terms.length > 0)
const fallbackCandidates = shouldFallback ? await this.externalFallback(terms.length ? terms : primary.capabilities, { providers: fallbackConfig.providers, maxResultsPerTerm: fallbackConfig.maxResultsPerTerm, ...externalOptions }) : []
return { ...primary, fallback: { triggered: shouldFallback, terms: terms.length ? terms : primary.capabilities, candidates: fallbackCandidates, warning: 'External candidates are discovery leads, not Reuseio-verified products. Read their official README, documentation, package metadata, and license before relying on them.' } }
}
}
export const createClient = (options?: ReuseioClientOptions) => new ReuseioClient(options)
src/version-sync.ts
typescriptexport type SyncDecision = 'up_to_date' | 'remote_newer' | 'local_newer' | 'invalid'
interface ParsedVersion { major: number; minor: number; patch: number; prerelease: string[] }
const parseVersion = (value: string): ParsedVersion | null => {
const match = value.trim().replace(/^v/i, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
if (!match) return null
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] ? match[4].split('.') : [] }
}
const comparePrerelease = (left: string[], right: string[]) => {
if (!left.length && !right.length) return 0
if (!left.length) return 1
if (!right.length) return -1
const length = Math.max(left.length, right.length)
for (let index = 0; index < length; index += 1) {
const a = left[index]
const b = right[index]
if (a === undefined) return -1
if (b === undefined) return 1
if (a === b) continue
const aNumber = /^\d+$/.test(a)
const bNumber = /^\d+$/.test(b)
if (aNumber && bNumber) return Number(a) - Number(b)
if (aNumber !== bNumber) return aNumber ? -1 : 1
return a < b ? -1 : 1
}
return 0
}
export const compareVersions = (left: string, right: string) => {
const a = parseVersion(left)
const b = parseVersion(right)
if (!a || !b) return Number.NaN
return a.major - b.major || a.minor - b.minor || a.patch - b.patch || comparePrerelease(a.prerelease, b.prerelease)
}
export const getSyncDecision = (localVersion: string, remoteVersion: string): SyncDecision => {
const comparison = compareVersions(localVersion, remoteVersion)
if (Number.isNaN(comparison)) return 'invalid'
if (comparison === 0) return 'up_to_date'
return comparison < 0 ? 'remote_newer' : 'local_newer'
}
src/mcp.ts
typescriptimport { McpServer } from '@modelcontextprotocol/server'
import { serveStdio } from '@modelcontextprotocol/server/stdio'
import * as z from 'zod/v4'
import { createClient, type ExternalCandidate, type OfficialReleaseManifest, type ResearchRequest, type ResearchResult, type ReuseioClient } from './index.js'
/** The compatibility revision used by the exported low-level JSON-RPC helper. */
export const MCP_PROTOCOL_VERSION = '2025-11-25'
type JsonRpcId = string | number | null
type JsonRpcRequest = { jsonrpc?: string; id?: JsonRpcId; method?: string; params?: Record<string, unknown> }
type RegistryClient = Pick<ReuseioClient, 'search' | 'searchMany' | 'researchTask' | 'getManifest' | 'getSources' | 'getAiPrompt' | 'getOfficialVersions'>
export const MCP_TOOL_DEFINITIONS = [
{
name: 'reuseio_search',
description: 'Search verified Reuseio products by capability, first-level Category, parallel tag facets, product name, provider, or documentation term.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', minLength: 1, description: 'Search query.' },
page: { type: 'integer', minimum: 1, description: 'One-based result page.' },
per_page: { type: 'integer', minimum: 1, maximum: 100, description: 'Results per page.' },
type: { type: 'string', description: 'Optional product type filter.' },
},
required: ['query'],
additionalProperties: false,
},
},
{
name: 'reuseio_search_many',
description: 'Search several Reuseio capability phrases in parallel and return deduplicated candidates.',
inputSchema: {
type: 'object',
properties: {
queries: { type: 'array', minItems: 1, maxItems: 12, items: { type: 'string', minLength: 1 } },
per_page: { type: 'integer', minimum: 1, maximum: 50 },
type: { type: 'string' },
max_results: { type: 'integer', minimum: 1, maximum: 100 },
},
required: ['queries'],
additionalProperties: false,
},
},
{
name: 'reuseio_research',
description: 'Research a technology-selection task with required/preferred capability coverage, separate first-level Category navigation and parallel Platform/Runtime/Deployment/Ecosystem facets, official evidence, and targeted npm keyword/GitHub topic fallback leads.',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string', minLength: 3, maxLength: 20000 },
capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
required_capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
preferred_capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
max_products: { type: 'integer', minimum: 1, maximum: 20 },
force: { type: 'boolean' },
fallback: { oneOf: [{ type: 'boolean' }, { type: 'object' }] },
},
required: ['task'],
additionalProperties: false,
},
},
{
name: 'reuseio_get_manifest',
description: 'Get the verified product manifest, including capabilities, first-level categories, parallel tags, and official sources.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_sources',
description: 'Get the complete verified source list for a Reuseio product.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_ai_prompt',
description: 'Get the implementation prompt grounded in a verified Reuseio product manifest.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_official_versions',
description: 'Get the official Skill and npm release manifest for version comparison.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
},
] as const
const isRecord = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
const stringValue = (value: unknown, name: string, min = 1, max = 20_000) => {
if (typeof value !== 'string' || value.trim().length < min || value.trim().length > max) throw new Error(`${name} must be a string with ${min}-${max} characters`)
return value.trim()
}
const stringArray = (value: unknown, name: string, max = 12) => {
if (value === undefined) return undefined
if (!Array.isArray(value) || value.length > max || value.some(item => typeof item !== 'string' || item.trim().length < 2)) throw new Error(`${name} must be an array of at most ${max} non-empty strings`)
return value.map(item => String(item).trim())
}
const integerValue = (value: unknown, name: string, fallback: number, min: number, max: number) => {
if (value === undefined) return fallback
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) throw new Error(`${name} must be an integer between ${min} and ${max}`)
return Number(value)
}
const jsonText = (value: unknown) => JSON.stringify(value, null, 2)
const textResult = (value: unknown, isError = false) => ({ content: [{ type: 'text' as const, text: jsonText(value) }], ...(isError ? { isError: true } : {}) })
const callTool = async (name: string, rawArguments: unknown, client: RegistryClient) => {
const args = isRecord(rawArguments) ? rawArguments : {}
switch (name) {
case 'reuseio_search': {
const query = stringValue(args.query, 'query', 1, 200)
const page = integerValue(args.page, 'page', 1, 1, 10_000)
const perPage = integerValue(args.per_page, 'per_page', 20, 1, 100)
const type = args.type === undefined ? undefined : stringValue(args.type, 'type', 1, 80)
return client.search(query, { page, perPage, type })
}
case 'reuseio_search_many': {
const queries = stringArray(args.queries, 'queries', 12)
if (!queries?.length) throw new Error('queries must contain at least one string')
return client.searchMany(queries, {
perPage: integerValue(args.per_page, 'per_page', 10, 1, 50),
type: args.type === undefined ? undefined : stringValue(args.type, 'type', 1, 80),
maxResults: integerValue(args.max_results, 'max_results', 20, 1, 100),
})
}
case 'reuseio_research': {
const request: ResearchRequest = {
task: stringValue(args.task, 'task', 3, 20_000),
capabilities: stringArray(args.capabilities, 'capabilities'),
requiredCapabilities: stringArray(args.required_capabilities, 'required_capabilities'),
preferredCapabilities: stringArray(args.preferred_capabilities, 'preferred_capabilities'),
maxProducts: integerValue(args.max_products, 'max_products', 20, 1, 20),
force: args.force === undefined ? false : Boolean(args.force),
fallback: typeof args.fallback === 'boolean' || isRecord(args.fallback) ? args.fallback as ResearchRequest['fallback'] : undefined,
}
return client.researchTask(request)
}
case 'reuseio_get_manifest': return client.getManifest(stringValue(args.slug, 'slug', 1, 200))
case 'reuseio_get_sources': return client.getSources(stringValue(args.slug, 'slug', 1, 200))
case 'reuseio_get_ai_prompt': return client.getAiPrompt(stringValue(args.slug, 'slug', 1, 200))
case 'reuseio_get_official_versions': return client.getOfficialVersions()
default: throw new Error(`Unknown tool: ${name}`)
}
}
const resultForTool = async (name: string, args: Record<string, unknown>, client: RegistryClient) => {
try {
return textResult(await callTool(name, args, client))
} catch (error) {
return textResult({ error: error instanceof Error ? error.message : String(error) }, true)
}
}
/** Build an MCP SDK server. The official SDK owns protocol negotiation and stdio framing. */
export const createMcpServer = (client: RegistryClient = defaultClient()) => {
const server = new McpServer({ name: 'reuseio', version: '0.5.1' }, { capabilities: { tools: {} } })
server.registerTool('reuseio_search', {
description: MCP_TOOL_DEFINITIONS[0].description,
inputSchema: z.object({ query: z.string().min(1).max(200), page: z.number().int().min(1).max(10_000).optional(), per_page: z.number().int().min(1).max(100).optional(), type: z.string().min(1).max(80).optional() }),
}, args => resultForTool('reuseio_search', args as Record<string, unknown>, client))
server.registerTool('reuseio_search_many', {
description: MCP_TOOL_DEFINITIONS[1].description,
inputSchema: z.object({ queries: z.array(z.string().min(1)).min(1).max(12), per_page: z.number().int().min(1).max(50).optional(), type: z.string().min(1).max(80).optional(), max_results: z.number().int().min(1).max(100).optional() }),
}, args => resultForTool('reuseio_search_many', args as Record<string, unknown>, client))
server.registerTool('reuseio_research', {
description: MCP_TOOL_DEFINITIONS[2].description,
inputSchema: z.object({
task: z.string().min(3).max(20_000),
capabilities: z.array(z.string().min(2)).max(12).optional(),
required_capabilities: z.array(z.string().min(2)).max(12).optional(),
preferred_capabilities: z.array(z.string().min(2)).max(12).optional(),
max_products: z.number().int().min(1).max(20).optional(),
force: z.boolean().optional(),
fallback: z.union([z.boolean(), z.object({ providers: z.array(z.enum(['github', 'npm'])).max(2).optional(), maxResultsPerTerm: z.number().int().min(1).max(12).optional() })]).optional(),
}),
}, args => resultForTool('reuseio_research', args as Record<string, unknown>, client))
server.registerTool('reuseio_get_manifest', {
description: MCP_TOOL_DEFINITIONS[3].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_manifest', args as Record<string, unknown>, client))
server.registerTool('reuseio_get_sources', {
description: MCP_TOOL_DEFINITIONS[4].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_sources', args as Record<string, unknown>, client))
server.registerTool('reuseio_get_ai_prompt', {
description: MCP_TOOL_DEFINITIONS[5].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_ai_prompt', args as Record<string, unknown>, client))
server.registerTool('reuseio_get_official_versions', {
description: MCP_TOOL_DEFINITIONS[6].description,
inputSchema: z.object({}),
}, () => resultForTool('reuseio_get_official_versions', {}, client))
return server
}
/** Handle one MCP JSON-RPC request; exported for deterministic contract tests. */
const defaultClient = () => createClient({ baseUrl: process.env.REUSEIO_API_URL })
export const handleMcpRequest = async (request: JsonRpcRequest, client: RegistryClient = defaultClient()): Promise<Record<string, unknown> | null> => {
if (!isRecord(request) || request.jsonrpc !== '2.0' || typeof request.method !== 'string') return { jsonrpc: '2.0', id: request?.id ?? null, error: { code: -32600, message: 'Invalid Request' } }
const id = request.id ?? null
if (request.method === 'notifications/initialized' || request.method.startsWith('notifications/')) return null
if (request.method === 'initialize') {
return {
jsonrpc: '2.0', id,
result: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: 'reuseio', version: '0.5.1' },
instructions: 'Use reuseio_research for technology selection. Treat Category as first-level navigation and keep Platform, Runtime, Deployment, and Ecosystem as parallel facets. Verify matchedCapabilities and official sources before making a decision.',
},
}
}
if (request.method === 'ping') return { jsonrpc: '2.0', id, result: {} }
if (request.method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools: MCP_TOOL_DEFINITIONS } }
if (request.method === 'tools/call') {
const params = isRecord(request.params) ? request.params : {}
const name = typeof params.name === 'string' ? params.name : ''
try {
return { jsonrpc: '2.0', id, result: textResult(await callTool(name, params.arguments, client)) }
} catch (error) {
return { jsonrpc: '2.0', id, result: textResult({ error: error instanceof Error ? error.message : String(error) }, true) }
}
}
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${request.method}` } }
}
/** Run the read-only MCP server over the official MCP stdio transport. */
export async function runMcpServer(client: RegistryClient = defaultClient()) {
serveStdio(() => createMcpServer(client), { onerror: error => console.error(error.message) })
}
export type { ExternalCandidate, OfficialReleaseManifest, ResearchResult }
dist/mcp.js
javascriptimport { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
import { createClient } from './index.js';
/** The compatibility revision used by the exported low-level JSON-RPC helper. */
export const MCP_PROTOCOL_VERSION = '2025-11-25';
export const MCP_TOOL_DEFINITIONS = [
{
name: 'reuseio_search',
description: 'Search verified Reuseio products by capability, first-level Category, parallel tag facets, product name, provider, or documentation term.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', minLength: 1, description: 'Search query.' },
page: { type: 'integer', minimum: 1, description: 'One-based result page.' },
per_page: { type: 'integer', minimum: 1, maximum: 100, description: 'Results per page.' },
type: { type: 'string', description: 'Optional product type filter.' },
},
required: ['query'],
additionalProperties: false,
},
},
{
name: 'reuseio_search_many',
description: 'Search several Reuseio capability phrases in parallel and return deduplicated candidates.',
inputSchema: {
type: 'object',
properties: {
queries: { type: 'array', minItems: 1, maxItems: 12, items: { type: 'string', minLength: 1 } },
per_page: { type: 'integer', minimum: 1, maximum: 50 },
type: { type: 'string' },
max_results: { type: 'integer', minimum: 1, maximum: 100 },
},
required: ['queries'],
additionalProperties: false,
},
},
{
name: 'reuseio_research',
description: 'Research a technology-selection task with required/preferred capability coverage, separate first-level Category navigation and parallel Platform/Runtime/Deployment/Ecosystem facets, official evidence, and targeted npm keyword/GitHub topic fallback leads.',
inputSchema: {
type: 'object',
properties: {
task: { type: 'string', minLength: 3, maxLength: 20000 },
capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
required_capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
preferred_capabilities: { type: 'array', maxItems: 12, items: { type: 'string', minLength: 2 } },
max_products: { type: 'integer', minimum: 1, maximum: 20 },
force: { type: 'boolean' },
fallback: { oneOf: [{ type: 'boolean' }, { type: 'object' }] },
},
required: ['task'],
additionalProperties: false,
},
},
{
name: 'reuseio_get_manifest',
description: 'Get the verified product manifest, including capabilities, first-level categories, parallel tags, and official sources.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_sources',
description: 'Get the complete verified source list for a Reuseio product.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_ai_prompt',
description: 'Get the implementation prompt grounded in a verified Reuseio product manifest.',
inputSchema: { type: 'object', properties: { slug: { type: 'string', minLength: 1 } }, required: ['slug'], additionalProperties: false },
},
{
name: 'reuseio_get_official_versions',
description: 'Get the official Skill and npm release manifest for version comparison.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
},
];
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
const stringValue = (value, name, min = 1, max = 20_000) => {
if (typeof value !== 'string' || value.trim().length < min || value.trim().length > max)
throw new Error(`${name} must be a string with ${min}-${max} characters`);
return value.trim();
};
const stringArray = (value, name, max = 12) => {
if (value === undefined)
return undefined;
if (!Array.isArray(value) || value.length > max || value.some(item => typeof item !== 'string' || item.trim().length < 2))
throw new Error(`${name} must be an array of at most ${max} non-empty strings`);
return value.map(item => String(item).trim());
};
const integerValue = (value, name, fallback, min, max) => {
if (value === undefined)
return fallback;
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max)
throw new Error(`${name} must be an integer between ${min} and ${max}`);
return Number(value);
};
const jsonText = (value) => JSON.stringify(value, null, 2);
const textResult = (value, isError = false) => ({ content: [{ type: 'text', text: jsonText(value) }], ...(isError ? { isError: true } : {}) });
const callTool = async (name, rawArguments, client) => {
const args = isRecord(rawArguments) ? rawArguments : {};
switch (name) {
case 'reuseio_search': {
const query = stringValue(args.query, 'query', 1, 200);
const page = integerValue(args.page, 'page', 1, 1, 10_000);
const perPage = integerValue(args.per_page, 'per_page', 20, 1, 100);
const type = args.type === undefined ? undefined : stringValue(args.type, 'type', 1, 80);
return client.search(query, { page, perPage, type });
}
case 'reuseio_search_many': {
const queries = stringArray(args.queries, 'queries', 12);
if (!queries?.length)
throw new Error('queries must contain at least one string');
return client.searchMany(queries, {
perPage: integerValue(args.per_page, 'per_page', 10, 1, 50),
type: args.type === undefined ? undefined : stringValue(args.type, 'type', 1, 80),
maxResults: integerValue(args.max_results, 'max_results', 20, 1, 100),
});
}
case 'reuseio_research': {
const request = {
task: stringValue(args.task, 'task', 3, 20_000),
capabilities: stringArray(args.capabilities, 'capabilities'),
requiredCapabilities: stringArray(args.required_capabilities, 'required_capabilities'),
preferredCapabilities: stringArray(args.preferred_capabilities, 'preferred_capabilities'),
maxProducts: integerValue(args.max_products, 'max_products', 20, 1, 20),
force: args.force === undefined ? false : Boolean(args.force),
fallback: typeof args.fallback === 'boolean' || isRecord(args.fallback) ? args.fallback : undefined,
};
return client.researchTask(request);
}
case 'reuseio_get_manifest': return client.getManifest(stringValue(args.slug, 'slug', 1, 200));
case 'reuseio_get_sources': return client.getSources(stringValue(args.slug, 'slug', 1, 200));
case 'reuseio_get_ai_prompt': return client.getAiPrompt(stringValue(args.slug, 'slug', 1, 200));
case 'reuseio_get_official_versions': return client.getOfficialVersions();
default: throw new Error(`Unknown tool: ${name}`);
}
};
const resultForTool = async (name, args, client) => {
try {
return textResult(await callTool(name, args, client));
}
catch (error) {
return textResult({ error: error instanceof Error ? error.message : String(error) }, true);
}
};
/** Build an MCP SDK server. The official SDK owns protocol negotiation and stdio framing. */
export const createMcpServer = (client = defaultClient()) => {
const server = new McpServer({ name: 'reuseio', version: '0.5.1' }, { capabilities: { tools: {} } });
server.registerTool('reuseio_search', {
description: MCP_TOOL_DEFINITIONS[0].description,
inputSchema: z.object({ query: z.string().min(1).max(200), page: z.number().int().min(1).max(10_000).optional(), per_page: z.number().int().min(1).max(100).optional(), type: z.string().min(1).max(80).optional() }),
}, args => resultForTool('reuseio_search', args, client));
server.registerTool('reuseio_search_many', {
description: MCP_TOOL_DEFINITIONS[1].description,
inputSchema: z.object({ queries: z.array(z.string().min(1)).min(1).max(12), per_page: z.number().int().min(1).max(50).optional(), type: z.string().min(1).max(80).optional(), max_results: z.number().int().min(1).max(100).optional() }),
}, args => resultForTool('reuseio_search_many', args, client));
server.registerTool('reuseio_research', {
description: MCP_TOOL_DEFINITIONS[2].description,
inputSchema: z.object({
task: z.string().min(3).max(20_000),
capabilities: z.array(z.string().min(2)).max(12).optional(),
required_capabilities: z.array(z.string().min(2)).max(12).optional(),
preferred_capabilities: z.array(z.string().min(2)).max(12).optional(),
max_products: z.number().int().min(1).max(20).optional(),
force: z.boolean().optional(),
fallback: z.union([z.boolean(), z.object({ providers: z.array(z.enum(['github', 'npm'])).max(2).optional(), maxResultsPerTerm: z.number().int().min(1).max(12).optional() })]).optional(),
}),
}, args => resultForTool('reuseio_research', args, client));
server.registerTool('reuseio_get_manifest', {
description: MCP_TOOL_DEFINITIONS[3].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_manifest', args, client));
server.registerTool('reuseio_get_sources', {
description: MCP_TOOL_DEFINITIONS[4].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_sources', args, client));
server.registerTool('reuseio_get_ai_prompt', {
description: MCP_TOOL_DEFINITIONS[5].description,
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
}, args => resultForTool('reuseio_get_ai_prompt', args, client));
server.registerTool('reuseio_get_official_versions', {
description: MCP_TOOL_DEFINITIONS[6].description,
inputSchema: z.object({}),
}, () => resultForTool('reuseio_get_official_versions', {}, client));
return server;
};
/** Handle one MCP JSON-RPC request; exported for deterministic contract tests. */
const defaultClient = () => createClient({ baseUrl: process.env.REUSEIO_API_URL });
export const handleMcpRequest = async (request, client = defaultClient()) => {
if (!isRecord(request) || request.jsonrpc !== '2.0' || typeof request.method !== 'string')
return { jsonrpc: '2.0', id: request?.id ?? null, error: { code: -32600, message: 'Invalid Request' } };
const id = request.id ?? null;
if (request.method === 'notifications/initialized' || request.method.startsWith('notifications/'))
return null;
if (request.method === 'initialize') {
return {
jsonrpc: '2.0', id,
result: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: 'reuseio', version: '0.5.1' },
instructions: 'Use reuseio_research for technology selection. Treat Category as first-level navigation and keep Platform, Runtime, Deployment, and Ecosystem as parallel facets. Verify matchedCapabilities and official sources before making a decision.',
},
};
}
if (request.method === 'ping')
return { jsonrpc: '2.0', id, result: {} };
if (request.method === 'tools/list')
return { jsonrpc: '2.0', id, result: { tools: MCP_TOOL_DEFINITIONS } };
if (request.method === 'tools/call') {
const params = isRecord(request.params) ? request.params : {};
const name = typeof params.name === 'string' ? params.name : '';
try {
return { jsonrpc: '2.0', id, result: textResult(await callTool(name, params.arguments, client)) };
}
catch (error) {
return { jsonrpc: '2.0', id, result: textResult({ error: error instanceof Error ? error.message : String(error) }, true) };
}
}
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${request.method}` } };
};
/** Run the read-only MCP server over the official MCP stdio transport. */
export async function runMcpServer(client = defaultClient()) {
serveStdio(() => createMcpServer(client), { onerror: error => console.error(error.message) });
}
dist/mcp.d.ts
typescriptimport { McpServer } from '@modelcontextprotocol/server';
import { type ExternalCandidate, type OfficialReleaseManifest, type ResearchResult, type ReuseioClient } from './index.js';
/** The compatibility revision used by the exported low-level JSON-RPC helper. */
export declare const MCP_PROTOCOL_VERSION = "2025-11-25";
type JsonRpcId = string | number | null;
type JsonRpcRequest = {
jsonrpc?: string;
id?: JsonRpcId;
method?: string;
params?: Record<string, unknown>;
};
type RegistryClient = Pick<ReuseioClient, 'search' | 'searchMany' | 'researchTask' | 'getManifest' | 'getSources' | 'getAiPrompt' | 'getOfficialVersions'>;
export declare const MCP_TOOL_DEFINITIONS: readonly [{
readonly name: "reuseio_search";
readonly description: "Search verified Reuseio products by capability, first-level Category, parallel tag facets, product name, provider, or documentation term.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly query: {
readonly type: "string";
readonly minLength: 1;
readonly description: "Search query.";
};
readonly page: {
readonly type: "integer";
readonly minimum: 1;
readonly description: "One-based result page.";
};
readonly per_page: {
readonly type: "integer";
readonly minimum: 1;
readonly maximum: 100;
readonly description: "Results per page.";
};
readonly type: {
readonly type: "string";
readonly description: "Optional product type filter.";
};
};
readonly required: readonly ["query"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_search_many";
readonly description: "Search several Reuseio capability phrases in parallel and return deduplicated candidates.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly queries: {
readonly type: "array";
readonly minItems: 1;
readonly maxItems: 12;
readonly items: {
readonly type: "string";
readonly minLength: 1;
};
};
readonly per_page: {
readonly type: "integer";
readonly minimum: 1;
readonly maximum: 50;
};
readonly type: {
readonly type: "string";
};
readonly max_results: {
readonly type: "integer";
readonly minimum: 1;
readonly maximum: 100;
};
};
readonly required: readonly ["queries"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_research";
readonly description: "Research a technology-selection task with required/preferred capability coverage, separate first-level Category navigation and parallel Platform/Runtime/Deployment/Ecosystem facets, official evidence, and targeted npm keyword/GitHub topic fallback leads.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly task: {
readonly type: "string";
readonly minLength: 3;
readonly maxLength: 20000;
};
readonly capabilities: {
readonly type: "array";
readonly maxItems: 12;
readonly items: {
readonly type: "string";
readonly minLength: 2;
};
};
readonly required_capabilities: {
readonly type: "array";
readonly maxItems: 12;
readonly items: {
readonly type: "string";
readonly minLength: 2;
};
};
readonly preferred_capabilities: {
readonly type: "array";
readonly maxItems: 12;
readonly items: {
readonly type: "string";
readonly minLength: 2;
};
};
readonly max_products: {
readonly type: "integer";
readonly minimum: 1;
readonly maximum: 20;
};
readonly force: {
readonly type: "boolean";
};
readonly fallback: {
readonly oneOf: readonly [{
readonly type: "boolean";
}, {
readonly type: "object";
}];
};
};
readonly required: readonly ["task"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_get_manifest";
readonly description: "Get the verified product manifest, including capabilities, first-level categories, parallel tags, and official sources.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly slug: {
readonly type: "string";
readonly minLength: 1;
};
};
readonly required: readonly ["slug"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_get_sources";
readonly description: "Get the complete verified source list for a Reuseio product.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly slug: {
readonly type: "string";
readonly minLength: 1;
};
};
readonly required: readonly ["slug"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_get_ai_prompt";
readonly description: "Get the implementation prompt grounded in a verified Reuseio product manifest.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly slug: {
readonly type: "string";
readonly minLength: 1;
};
};
readonly required: readonly ["slug"];
readonly additionalProperties: false;
};
}, {
readonly name: "reuseio_get_official_versions";
readonly description: "Get the official Skill and npm release manifest for version comparison.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {};
readonly additionalProperties: false;
};
}];
/** Build an MCP SDK server. The official SDK owns protocol negotiation and stdio framing. */
export declare const createMcpServer: (client?: RegistryClient) => McpServer;
export declare const handleMcpRequest: (request: JsonRpcRequest, client?: RegistryClient) => Promise<Record<string, unknown> | null>;
/** Run the read-only MCP server over the official MCP stdio transport. */
export declare function runMcpServer(client?: RegistryClient): Promise<void>;
export type { ExternalCandidate, OfficialReleaseManifest, ResearchResult };