MCP connection

Reuseio MCP

v0.5.1

The read-only MCP connection distributed through npm, exposing Reuseio search, research, source, and version tools.

13 filesManifest 1.0.0Official content snapshot

MCP CONNECTION COMMAND

npx -y --package reuseio@latest reuseio-mcp

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

javascript
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 = []) => [...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

typescript
export 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

javascript
const 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

typescript
export 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

typescript
export 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

typescript
export 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

typescript
import { 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

javascript
import { 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

typescript
import { 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 };