Add doc-weaver: TypeScript documentation compiler

TypeDoc + JSDoc (RDFC) compiler mirroring doc-forge. Emits a
doc-forge-shaped MCP JSON bundle and a TypeDoc human site, with an MCP
server (official SDK, stdio), a build/serve CLI, and React-aware kind
classification. Includes the RDFC wiki (docs/wiki) and MkDocs config.
This commit is contained in:
2026-10-18 12:17:18 +05:30
parent bb3767ac15
commit f7a997b4ce
23 changed files with 3404 additions and 0 deletions

138
src/cli/commands.ts Normal file
View File

@@ -0,0 +1,138 @@
import { Command } from 'commander';
import * as path from 'path';
import fs from 'fs-extra';
import { runTypeDocJson } from '../loaders/typedoc-loader.js';
import { generateHumanDocs } from '../renderers/human-docs.js';
import { generateMcpResources, serveMcp } from './mcp-utils.js';
/**
* Build documentation.
*
* Mirrors doc-forge's `build` command:
* doc-weaver build --mcp --docs --module <entry>
*/
export function buildCommand(): Command {
return new Command('build')
.description('Build documentation (MCP JSON and/or a TypeDoc human site)')
.option('--mcp', 'Generate MCP JSON resources (index/nav/modules)')
.option('--docs', 'Generate the human-facing TypeDoc site')
.option('--mkdocs', 'Alias for --docs (MkDocs parallel)')
.option('--lib', 'Alias for --docs (library site)')
.option('--api', 'API docs from an OpenAPI spec (reserved)')
.option('--wiki', 'Hand-written wiki build (reserved)')
.option('--module <path>', 'Entry point (file or directory) for TypeDoc')
.option('--entry <file>', 'Entry file (alternative to --module)')
.option('--project-name <name>', 'Project name override')
.option('--name <name>', 'Site name (alias of --project-name)')
.option('--tsconfig <file>', 'tsconfig.json path')
.option('--out-dir <dir>', 'MCP output directory', 'docs/mcp')
.option('--out <dir>', 'Human docs output directory', 'site/lib')
.option('--plugin <name...>', 'TypeDoc plugins to enable')
.option('--strict', 'Fail on TypeScript errors instead of generating anyway')
.action(async (opts) => {
const entry = opts.module || opts.entry;
if (!entry) {
throw new Error('Must specify --module or --entry');
}
const entryAbs = path.resolve(entry);
const projectName =
opts.projectName ||
opts.name ||
(await inferProjectName(entryAbs));
const wantMcp = opts.mcp;
const wantDocs = opts.docs || opts.mkdocs || opts.lib;
const wantApi = opts.api;
const wantWiki = opts.wiki;
if (!wantMcp && !wantDocs && !wantApi && !wantWiki) {
throw new Error('Must specify at least one of --mcp, --docs, --api, --wiki');
}
if (wantApi || wantWiki) {
throw new Error('--api and --wiki are reserved and not yet implemented');
}
const tsconfig = resolveTsconfig(entryAbs, opts.tsconfig);
// Directory entries expand to every file; a single file uses resolve.
const entryPointStrategy = fs.statSync(entryAbs).isDirectory() ? 'expand' : 'resolve';
if (wantMcp) {
const json = await runTypeDocJson({
entryPoints: [entryAbs],
tsconfig,
name: projectName,
skipErrorChecking: !opts.strict,
entryPointStrategy
});
const outDir = path.resolve(opts.outDir);
await generateMcpResources(json, projectName, outDir);
console.log(`MCP resources written to ${outDir}`);
}
if (wantDocs) {
const out = await generateHumanDocs({
entryPoints: [entryAbs],
tsconfig,
name: projectName,
out: path.resolve(opts.out),
plugin: opts.plugin,
skipErrorChecking: !opts.strict,
entryPointStrategy
});
console.log(`Human docs written to ${out}`);
}
});
}
/** Serve documentation locally. */
export function serveCommand(): Command {
return new Command('serve')
.description('Serve documentation locally')
.option('--mcp', 'Serve the generated MCP bundle')
.option('--module <name>', 'Module name used to label the MCP server')
.option('--out-dir <dir>', 'MCP root directory', 'docs/mcp')
.action(async (opts) => {
if (!opts.mcp) {
throw new Error('Must specify --mcp');
}
if (!opts.module) {
throw new Error('--module is required for MCP serve');
}
await serveMcp(opts.module, path.resolve(opts.outDir));
});
}
function resolveTsconfig(entryAbs: string, explicit?: string): string | undefined {
if (explicit) {
return fs.existsSync(explicit) ? path.resolve(explicit) : undefined;
}
let dir = path.dirname(entryAbs);
for (let i = 0; i < 6; i++) {
const candidate = path.join(dir, 'tsconfig.json');
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return undefined;
}
async function inferProjectName(entryAbs: string): Promise<string> {
let dir = path.dirname(entryAbs);
for (let i = 0; i < 6; i++) {
const pkg = path.join(dir, 'package.json');
if (await fs.pathExists(pkg)) {
try {
const data = await fs.readJSON(pkg);
if (data?.name) return data.name;
} catch {
// ignore malformed package.json
}
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return path.basename(path.dirname(entryAbs));
}

24
src/cli/index.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Command } from 'commander';
import { pathToFileURL } from 'url';
import { buildCommand, serveCommand } from './commands.js';
export function main(): void {
const program = new Command();
program
.name('doc-weaver')
.description('TypeScript documentation compiler: MkDocs + MCP (doc-forge compatible)')
.version('0.1.0');
program.addCommand(buildCommand());
program.addCommand(serveCommand());
program.parseAsync(process.argv).catch((err) => {
console.error(err);
process.exit(1);
});
}
// Allow `tsx src/cli/index.ts` and `node dist/cli/index.js` to run directly.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}

41
src/cli/mcp-utils.ts Normal file
View File

@@ -0,0 +1,41 @@
import { TypeDocJsonToMCP } from '../renderers/mcp-renderer.js';
import { MCPServer } from '../servers/mcp-server.js';
import * as path from 'path';
import fs from 'fs-extra';
/** Generate the doc-forge-compatible MCP bundle from TypeDoc reflection JSON. */
export async function generateMcpResources(
typedocJson: any,
projectName: string | undefined,
outDir: string
): Promise<void> {
const transformer = new TypeDocJsonToMCP(typedocJson, projectName);
const { index, nav, modules } = transformer.transform();
await fs.ensureDir(path.join(outDir, 'modules'));
await fs.writeJSON(path.join(outDir, 'index.json'), index, { spaces: 2 });
await fs.writeJSON(path.join(outDir, 'nav.json'), nav, { spaces: 2 });
for (const [modPath, modData] of Object.entries(modules)) {
const outPath = path.join(outDir, 'modules', modPath + '.json');
await fs.ensureDir(path.dirname(outPath));
await fs.writeJSON(outPath, modData, { spaces: 2 });
}
}
/** Serve a pre-generated MCP bundle over stdio/HTTP. */
export async function serveMcp(module: string, mcpRoot: string): Promise<void> {
const root = path.resolve(mcpRoot);
const required = [
path.join(root, 'index.json'),
path.join(root, 'nav.json'),
path.join(root, 'modules')
];
for (const r of required) {
if (!(await fs.pathExists(r))) {
throw new Error('Invalid MCP bundle, missing: ' + path.basename(r));
}
}
const server = new MCPServer(root, module + '-mcp');
await server.run();
}

29
src/index.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* # doc-weaver
*
* Public library surface for doc-weaver, the TypeScript counterpart to
* `doc-forge`. It compiles TypeDoc reflection (built from TypeScript + JSDoc)
* into two renderers:
*
* - a human-facing TypeDoc site (the MkDocs parallel), and
* - a machine-facing MCP JSON bundle (`index.json`, `nav.json`, `modules/`)
* whose shape is byte-compatible with `doc-forge`.
*
* Programmatic usage:
*
* import { runTypeDocJson, TypeDocJsonToMCP } from 'doc-weaver';
* const json = await runTypeDocJson({ entryPoints: ['src/index.ts'] });
* const bundle = new TypeDocJsonToMCP(json, 'my-app').transform();
*/
export { main } from './cli/index.js';
export { runTypeDocJson, defaultTsconfigFor } from './loaders/typedoc-loader.js';
export type { TypeDocOptions } from './loaders/typedoc-loader.js';
export { generateHumanDocs } from './renderers/human-docs.js';
export type { HumanDocsOptions } from './renderers/human-docs.js';
export { TypeDocJsonToMCP } from './renderers/mcp-renderer.js';
export type { McpBundle } from './renderers/mcp-renderer.js';
export { generateMcpResources, serveMcp } from './cli/mcp-utils.js';
export { MCPServer } from './servers/mcp-server.js';
export { collectModules, resolveModulePath, RefKind } from './models/typedoc.js';
export type { DocObject, DocModule, DocProject, DocKind } from './models/index.js';

View File

@@ -0,0 +1,61 @@
import { Application, TSConfigReader } from 'typedoc';
import * as path from 'path';
export interface TypeDocOptions {
entryPoints: string[];
tsconfig?: string;
name?: string;
/** Optional TypeDoc theme/plugin passthroughs */
plugin?: string[];
/**
* Skip TypeScript error checking before conversion. Defaults to true so
* documentation still generates when a project has unrelated type errors.
*/
skipErrorChecking?: boolean;
/** TypeDoc entry-point strategy. Defaults to 'resolve'. */
entryPointStrategy?: 'resolve' | 'expand' | 'merge' | 'packages';
}
/**
* Run TypeDoc programmatically and return the serialized reflection JSON.
*
* Mirrors doc-forge's static loader: no user code is executed for
* documentation semantics; we rely on TypeScript's compiler API.
*/
export async function runTypeDocJson(opts: TypeDocOptions): Promise<any> {
const basePath = opts.tsconfig ? path.dirname(opts.tsconfig) : opts.entryPoints[0];
const app = await Application.bootstrapWithPlugins(
{
entryPoints: opts.entryPoints,
tsconfig: opts.tsconfig,
name: opts.name,
plugin: opts.plugin,
basePath,
// Keep things deterministic and quiet
readme: 'none',
entryPointStrategy: opts.entryPointStrategy ?? 'resolve',
excludePrivate: false,
excludeInternal: false,
skipErrorChecking: opts.skipErrorChecking ?? true
},
[new TSConfigReader()]
);
const project = await app.convert();
if (!project) {
throw new Error('TypeDoc failed to produce a project reflection');
}
// TypeDoc >= 0.26 exposes serializer.projectToObject(root, projectRoot).
const serializer: any = (app as any).serializer;
if (serializer && typeof serializer.projectToObject === 'function') {
return serializer.projectToObject(project, basePath);
}
throw new Error('Unsupported TypeDoc version: serializer.projectToObject is unavailable');
}
/** Convenience: resolve tsconfig relative to a given project root. */
export function defaultTsconfigFor(root: string): string {
return path.join(root, 'tsconfig.json');
}

52
src/models/index.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* Semantic documentation model for doc-weaver.
*
* This mirrors doc-forge's renderer-agnostic model: a project is a
* collection of modules, each module contains named documentation objects,
* and objects may contain nested members. Renderers (MkDocs, MCP) consume
* this model so that output formats never depend on each other.
*/
/** The set of documentable symbol kinds recognized by doc-weaver. */
export type DocKind =
| 'module'
| 'namespace'
| 'class'
| 'interface'
| 'enum'
| 'enum-member'
| 'type-alias'
| 'variable'
| 'function'
| 'method'
| 'accessor'
| 'property'
| 'constructor'
| 'signature'
| 'parameter'
| 'component'
| 'hook'
| 'unknown';
/** A documentation object with an optional tree of members. */
export interface DocObject {
name: string;
kind: DocKind;
path: string;
signature: string;
docstring: string;
members?: Record<string, DocObject>;
}
/** A single module (typically one source file or package). */
export interface DocModule {
path: string;
docstring: string;
objects: Record<string, DocObject>;
}
/** The root semantic model produced by the loader. */
export interface DocProject {
name: string;
modules: DocModule[];
}

66
src/models/typedoc.ts Normal file
View File

@@ -0,0 +1,66 @@
import { ReflectionKind } from 'typedoc';
/** Re-export TypeDoc's numeric reflection kinds for internal use. */
export const RefKind = ReflectionKind;
/**
* Recursively collect every Module-kind reflection in a TypeDoc project.
*
* TypeDoc emits Module reflections for each resolved entry point (one per
* source file/package). When a project has no module children (a single flat
* entry point), the project itself is treated as a single module so that the
* output always has at least one module - matching doc-forge's model.
*/
export function collectModules(project: any): any[] {
const modules: any[] = [];
const walk = (node: any) => {
if (!node) return;
if (node.kind === RefKind.Module) {
modules.push(node);
return;
}
for (const child of node.children || []) walk(child);
};
walk(project);
if (modules.length > 0) return modules;
return [project];
}
/** Derive a stable, filesystem-safe module path from a TypeDoc reflection. */
export function resolveModulePath(mod: any, project: any): string {
const source = mod.sources?.[0]?.fileName;
if (typeof source === 'string' && source.length) {
return normalizeSourcePath(source);
}
if (mod.kind === RefKind.Project) {
return sanitize(mod.name || project?.name || 'project');
}
return sanitize(mod.name || 'module');
}
function normalizeSourcePath(fileName: string): string {
let p = fileName.replace(/\\/g, '/');
p = p.replace(/^\.\//, '');
p = p.replace(/^src\//, '');
p = p.replace(/\.(tsx?|jsx?|mts|cts|mjs|cjs)$/, '');
p = p.replace(/\/index$/, '');
// Use dotted paths so MCP resource templates match a single URI segment,
// mirroring doc-forge's dotted Python module paths.
p = p.replace(/\//g, '.');
return sanitize(p);
}
/** Replace characters that are unsafe in a resource path / filename. */
export function sanitize(value: string): string {
return value
.replace(/\\/g, '.')
.replace(/[^A-Za-z0-9_.-]/g, '_')
.replace(/^\.+/, '')
.replace(/\.+$/, '');
}
/** Human-readable name for a reflective kind (used in diagnostics). */
export function reflectionKindName(kind: number): string {
return ReflectionKind[kind] ?? String(kind);
}

115
src/renderers/docstring.ts Normal file
View File

@@ -0,0 +1,115 @@
import { ReflectionKind } from 'typedoc';
/**
* Convert a TypeDoc serialized comment into a Google-style markdown docstring.
*
* TypeDoc stores comments as a list of display parts (text/code/inline-tag)
* plus blockTags (e.g. @example, @returns, @param). This function renders
* them back to a stable markdown string so both MkDocs and MCP consumers
* receive the same human-readable documentation.
*/
export function renderComment(comment: any): string {
if (!comment) return '';
const lines: string[] = [];
const summary = renderParts(comment.summary);
if (summary.trim()) lines.push(summary.trim());
for (const tag of comment.blockTags || []) {
const tagName = (tag.tag || '').replace(/^@/, '');
const content = renderParts(tag.content).trim();
if (!content) continue;
const heading = tagHeading(tagName);
lines.push('');
lines.push(heading);
lines.push('');
lines.push(content);
}
return lines.join('\n').trim();
}
function tagHeading(tag: string): string {
switch (tag) {
case 'param':
return '**Parameters:**';
case 'returns':
case 'return':
return '**Returns:**';
case 'example':
return '**Example:**';
case 'throws':
return '**Throws:**';
case 'deprecated':
return '**Deprecated:**';
case 'see':
return '**See also:**';
case 'remarks':
return '**Remarks:**';
default:
return '**@' + tag + ':**';
}
}
/** Render a list of TypeDoc comment display parts into markdown. */
export function renderParts(parts: any[] | undefined): string {
if (!parts || !Array.isArray(parts)) return '';
const out: string[] = [];
for (const part of parts) {
if (typeof part === 'string') {
out.push(part);
continue;
}
if (!part) continue;
out.push(part.text ?? '');
}
return out.join('');
}
/** Map TypeDoc numerical ReflectionKind to doc-weaver DocKind. */
export function mapKind(kind: number): string {
switch (kind) {
case ReflectionKind.Module:
return 'module';
case ReflectionKind.Namespace:
return 'namespace';
case ReflectionKind.Class:
return 'class';
case ReflectionKind.Interface:
return 'interface';
case ReflectionKind.Enum:
return 'enum';
case ReflectionKind.EnumMember:
return 'enum-member';
case ReflectionKind.TypeAlias:
return 'type-alias';
case ReflectionKind.Variable:
return 'variable';
case ReflectionKind.Function:
return 'function';
case ReflectionKind.Method:
return 'method';
case ReflectionKind.Accessor:
return 'accessor';
case ReflectionKind.Property:
return 'property';
case ReflectionKind.Constructor:
return 'constructor';
case ReflectionKind.CallSignature:
case ReflectionKind.ConstructorSignature:
return 'signature';
case ReflectionKind.Parameter:
return 'parameter';
default:
return 'unknown';
}
}
/** Classify a symbol as a React component or hook where possible. */
export function classifyReact(name: string, kind: string): string {
if (kind === 'function' || kind === 'variable') {
if (/^use[A-Z0-9]/.test(name)) return 'hook';
if (/^[A-Z]/.test(name)) return 'component';
}
return kind;
}

View File

@@ -0,0 +1,50 @@
import { Application, TSConfigReader } from 'typedoc';
import * as path from 'path';
/**
* Generate the human-facing documentation site using TypeDoc.
*
* TypeDoc plays the same role for doc-weaver that MkDocs plays for
* doc-forge: it renders the compiled model into a browsable site. The
* machine-facing output (MCP JSON) is produced separately.
*/
export interface HumanDocsOptions {
entryPoints: string[];
tsconfig?: string;
name?: string;
out: string;
/** Optional path to a typedoc.json config. */
options?: string;
/** Optional TypeDoc plugins (e.g. typedoc-plugin-markdown). */
plugin?: string[];
/** Skip TypeScript error checking. Defaults to true. */
skipErrorChecking?: boolean;
/** TypeDoc entry-point strategy. Defaults to 'resolve'. */
entryPointStrategy?: 'resolve' | 'expand' | 'merge' | 'packages';
}
export async function generateHumanDocs(opts: HumanDocsOptions): Promise<string> {
const basePath = opts.tsconfig ? path.dirname(opts.tsconfig) : opts.entryPoints[0];
const app = await Application.bootstrapWithPlugins(
{
entryPoints: opts.entryPoints,
tsconfig: opts.tsconfig,
name: opts.name,
out: opts.out,
options: opts.options,
plugin: opts.plugin,
basePath,
readme: 'none',
entryPointStrategy: opts.entryPointStrategy ?? 'resolve',
skipErrorChecking: opts.skipErrorChecking ?? true
},
[new TSConfigReader()]
);
const project = await app.convert();
if (!project) {
throw new Error('TypeDoc failed to convert the project');
}
await app.generateDocs(project, opts.out);
return path.resolve(opts.out);
}

View File

@@ -0,0 +1,113 @@
import {
RefKind,
reflectionKindName,
resolveModulePath,
collectModules
} from '../models/typedoc.js';
import { renderComment, mapKind, classifyReact } from './docstring.js';
import { bestSignature } from './signature.js';
export interface McpBundle {
index: Record<string, unknown>;
nav: Array<{ module: string; resource: string }>;
modules: Record<string, { module: string; content: Record<string, unknown> }>;
}
/**
* Transform a TypeDoc serialized reflection JSON into a doc-forge-compatible
* MCP bundle.
*
* The produced shape matches doc-forge exactly:
*
* - `index.json` -> `{ project, type, modules_count, source }`
* - `nav.json` -> `[ { module, resource } ]` with `docs://modules/{module}`
* - `modules/*.json` -> `{ module, content: { path, docstring, objects } }`
*
* Objects are rendered recursively with
* `{ name, kind, path, signature, docstring, members? }`.
*/
export class TypeDocJsonToMCP {
constructor(
private readonly project: any,
private readonly projectName?: string
) {}
transform(): McpBundle {
const name = this.projectName || this.project?.name || 'project';
const moduleRefs = collectModules(this.project);
const nav: McpBundle['nav'] = [];
const modules: McpBundle['modules'] = {};
for (const mod of moduleRefs) {
const modPath = resolveModulePath(mod, this.project);
nav.push({ module: modPath, resource: `docs://modules/${modPath}` });
modules[modPath] = {
module: modPath,
content: this.renderModule(mod, modPath)
};
}
const index = {
project: name,
type: 'docforge-model',
modules_count: nav.length,
source: 'doc-weaver'
};
return { index, nav, modules };
}
private renderModule(mod: any, modPath: string): Record<string, unknown> {
const objects: Record<string, unknown> = {};
for (const child of mod.children || []) {
if (child.kind === RefKind.Module) continue;
objects[child.name] = this.renderObject(child, modPath);
}
return {
path: modPath,
docstring: renderComment(mod.comment),
objects
};
}
private renderObject(ref: any, parentPath: string): Record<string, unknown> {
const kindName = classifyReact(ref.name, mapKind(ref.kind));
const path = `${parentPath}.${ref.name}`;
const data: Record<string, unknown> = {
name: ref.name,
kind: kindName,
path,
signature: bestSignature(ref),
docstring: renderComment(ref.comment)
};
// Prefer an explicit @kind tag when present (mirrors doc-forge kinds).
const kindTag = findBlockTag(ref.comment, '@kind');
if (kindTag) data.kind = kindTag;
const members = this.collectMembers(ref, path);
if (Object.keys(members).length) data.members = members;
return data;
}
private collectMembers(ref: any, parentPath: string): Record<string, unknown> {
const members: Record<string, unknown> = {};
for (const child of ref.children || []) {
members[child.name] = this.renderObject(child, parentPath);
}
// Enum members and similar leaf children are already handled by children.
return members;
}
}
function findBlockTag(comment: any, tag: string): string | undefined {
for (const bt of comment?.blockTags || []) {
if (bt.tag === tag) {
return (bt.content || []).map((p: any) => p.text ?? '').join('').trim();
}
}
return undefined;
}

View File

@@ -0,0 +1,90 @@
/** Utilities for reconstructing human-readable signatures from TypeDoc JSON. */
/** Render a TypeDoc type object to a compact TypeScript-like string. */
export function renderType(type: any): string {
if (!type) return 'unknown';
if (typeof type === 'string') return type;
switch (type.type) {
case 'intrinsic':
return type.name ?? 'unknown';
case 'reference': {
const name = type.name ?? 'unknown';
const args = (type.typeArguments || []).map(renderType);
const qualified =
type.qualifiedName && type.qualifiedName !== name ? type.qualifiedName : name;
return args.length ? `${qualified}<${args.join(', ')}>` : qualified;
}
case 'array':
return `${renderType(type.elementType)}[]`;
case 'union':
return (type.types || []).map(renderType).join(' | ');
case 'intersection':
return (type.types || []).map(renderType).join(' & ');
case 'tuple':
return `[${(type.elements || []).map(renderType).join(', ')}]`;
case 'literal':
return type.value === null ? 'null' : JSON.stringify(type.value);
case 'reflection':
if (type.declaration?.signatures?.length) {
const sig = type.declaration.signatures[0];
return sig.type ? renderType(sig.type) : 'function';
}
if (type.declaration?.children?.length) {
const members = type.declaration.children
.map((c: any) => `${c.name}: ${renderType(c.type)}`)
.join('; ');
return `{ ${members} }`;
}
return 'object';
case 'typeParameter':
return type.name ?? 'T';
case 'predicate':
return type.name ?? 'boolean';
case 'query':
return `typeof ${renderType(type.queryType)}`;
case 'rest':
return `...${renderType(type.elementType)}`;
case 'namedTupleMember':
return `${type.name}: ${renderType(type.elementType)}`;
case 'optional':
return `${renderType(type.elementType)}?`;
default:
return type.name ?? 'unknown';
}
}
/** Render a function/call signature to a TypeScript-like string. */
export function renderSignature(signature: any, fallbackName?: string): string {
if (!signature) return fallbackName ?? '';
const name = signature.name ?? fallbackName ?? '';
const params = (signature.parameters || [])
.map((p: any) => {
const optional = p.flags?.isOptional ? '?' : '';
return `${p.name}${optional}: ${renderType(p.type)}`;
})
.join(', ');
const returns = signature.type ? `: ${renderType(signature.type)}` : '';
return `${name}(${params})${returns}`;
}
/**
* Choose the best signature string for a reflection.
*
* Functions/methods/constructors keep their declared signatures; variables
* and properties expose their type; classes fall back to their name.
*/
export function bestSignature(ref: any): string {
if (ref.signatures?.length) {
return ref.signatures.map((s: any) => renderSignature(s, ref.name)).join(' | ');
}
if (ref.getSignature || ref.setSignature) {
const get = ref.getSignature ? renderSignature(ref.getSignature, ref.name) : '';
const set = ref.setSignature ? `set ${renderSignature(ref.setSignature, ref.name)}` : '';
return [get, set].filter(Boolean).join(' | ');
}
if (ref.type) {
return `${ref.name}: ${renderType(ref.type)}`;
}
return ref.name ?? '';
}

82
src/servers/mcp-server.ts Normal file
View File

@@ -0,0 +1,82 @@
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as path from 'path';
import fs from 'fs-extra';
/**
* MCP server serving a pre-generated doc-weaver bundle.
*
* Mirrors `docforge.servers.MCPServer`: it exposes read-only documentation
* resources and a single health-check tool.
*
* Resources:
* - `docs://index` - project metadata
* - `docs://nav` - navigation structure
* - `docs://modules/{module}` - individual module documentation
*/
export class MCPServer {
private readonly server: McpServer;
constructor(
private readonly mcpRoot: string,
name: string
) {
this.server = new McpServer({ name, version: '0.1.0' });
this.registerResources();
this.registerTools();
}
private readJson(
relPath: string,
uri: string
): { contents: Array<{ uri: string; mimeType?: string; text: string }> } {
const full = path.join(this.mcpRoot, relPath);
let text: string;
if (fs.existsSync(full)) {
text = fs.readFileSync(full, 'utf-8');
} else {
text = JSON.stringify({ error: 'not_found', path: full }, null, 2);
}
return { contents: [{ uri, mimeType: 'application/json', text }] };
}
private registerResources(): void {
this.server.registerResource(
'index',
'docs://index',
{ title: 'Index', mimeType: 'application/json' },
async (uri) => this.readJson('index.json', uri.href)
);
this.server.registerResource(
'nav',
'docs://nav',
{ title: 'Navigation', mimeType: 'application/json' },
async (uri) => this.readJson('nav.json', uri.href)
);
const template = new ResourceTemplate('docs://modules/{module}', { list: undefined });
this.server.registerResource(
'module',
template,
{ title: 'Module', mimeType: 'application/json' },
async (uri, variables) => {
const module = String(variables.module);
return this.readJson(path.join('modules', module + '.json'), uri.href);
}
);
}
private registerTools(): void {
this.server.registerTool(
'ping',
{ description: 'Health check that returns "pong".' },
async () => ({ content: [{ type: 'text', text: 'pong' }] })
);
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}