Add doc-weaver 0.1.0: TypeScript documentation compiler
Some checks failed
continuous-integration/drone/tag Build is failing

TypeDoc + JSDoc (RDFC) compiler producing a TypeDoc site and a
doc-forge-shaped MCP bundle for React/TypeScript packages.

- TypeDoc loader + human-docs and MCP renderers, signature-level
  parameter docs and default-export surfacing
- MCPServer over @modelcontextprotocol/sdk (docs://index, docs://nav,
  docs://modules/{module}, ping tool)
- doc-weaver build / serve CLI
- RDFC convention guide and hand-written wiki
- packaged as @aetoskia/doc-weaver with an npm release pipeline
  (build-and-publish-npm + backfill-npm-from-tags)
This commit is contained in:
2026-10-21 00:35:15 +05:30
parent bb3767ac15
commit aeb6e6b0f6
24 changed files with 3688 additions and 0 deletions

156
.drone.yml Normal file
View File

@@ -0,0 +1,156 @@
---
kind: pipeline
type: docker
name: build-and-publish-npm
platform:
os: linux
arch: arm64
workspace:
path: /drone/src
steps:
- name: check-version
image: node:22-alpine
environment:
NPM_REPO_URL:
from_secret: NPM_REPO_URL
NPM_USERNAME:
from_secret: NPM_USERNAME
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache curl
- PACKAGE_NAME=$(node -p "require('./package.json').name")
- VERSION=$(node -p "require('./package.json').version")
- |
if [ "$DRONE_TAG" != "$VERSION" ]; then
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
exit 1
fi
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
- |
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
exit 78
else
echo "New version detected: $PACKAGE_NAME@$VERSION"
fi
- name: quality-gate
image: node:22-alpine
environment:
NPM_REPO_URL:
from_secret: NPM_REPO_URL
NPM_USERNAME:
from_secret: NPM_USERNAME
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- |
if [ -z "$NPM_REPO_URL" ] || [ -z "$NPM_USERNAME" ] || [ -z "$NPM_PASSWORD" ]; then
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
exit 1
fi
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- npm install
- echo "Running quality gate..."
- npm run quality-gate
- name: build-package
image: node:22-alpine
commands:
- npm install
- echo "Building npm package..."
- npm run build
- ls -l dist
- name: publish-to-private-npm
image: node:22-alpine
environment:
NPM_REPO_URL:
from_secret: NPM_REPO_URL
NPM_USERNAME:
from_secret: NPM_USERNAME
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- echo "Publishing to private npm at $NPM_REPO_URL ..."
- npm publish --access restricted
trigger:
event:
- tag
---
kind: pipeline
type: docker
name: backfill-npm-from-tags
platform:
os: linux
arch: arm64
workspace:
path: /drone/src
steps:
- name: fetch-tags
image: alpine/git
commands:
- git fetch --tags --force
- name: build-and-publish-missing
image: node:22-alpine
environment:
NPM_REPO_URL:
from_secret: NPM_REPO_URL
NPM_USERNAME:
from_secret: NPM_USERNAME
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache git
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- |
set -e
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Package: $PACKAGE_NAME"
for TAG in $(git tag --sort=version:refname); do
VERSION="$TAG"
echo "Version: $VERSION"
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
echo "Exists, skipping"
continue
fi
git checkout --force "$TAG"
TAG_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG" != "$TAG_VERSION" ]; then
echo "Tag $TAG != package.json version $TAG_VERSION at that commit; skipping"
continue
fi
echo "Building $VERSION"
rm -rf dist
npm install
npm run build
echo "Publishing $VERSION"
npm publish --access restricted --userconfig /tmp/.npmrc
done
trigger:
event:
- custom

35
CHANGELOG.md Normal file
View File

@@ -0,0 +1,35 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - 2026-10-21
### Added
- Initial npm release as `@aetoskia/doc-weaver` (npm.aetoskia.com).
- TypeDoc-based loader (`runTypeDocJson`) producing serialized reflection JSON.
- `TypeDocJsonToMCP` renderer emitting a doc-forge-compatible MCP bundle:
`index.json`, `nav.json`, and per-module JSON under `modules/` with dotted
module paths and `docs://` resource URIs.
- `generateHumanDocs` renderer producing the TypeDoc HTML site (the MkDocs
parallel).
- `MCPServer` built on the official `@modelcontextprotocol/sdk`, exposing
`docs://index`, `docs://nav`, and `docs://modules/{module}` plus a `ping`
tool.
- `doc-weaver build` and `doc-weaver serve` CLI commands.
- Signature-level parameter rendering and default-export surfacing in MCP
bundles.
- React-aware kind classification (`component`, `hook`) and Google-style
docstring rendering.
- RDFC convention guide under `docs/wiki/02_rdfc.md`, plus a hand-written
wiki (`docs/wiki/`) built with MkDocs into `site/wiki/`.
- Self-documentation: `docs/mcp/` bundle and `site/lib/` TypeDoc site for
`doc-weaver` itself.
[Unreleased]: https://git.aetoskia.com/aetos/doc-weaver/compare/0.1.0...HEAD
[0.1.0]: https://git.aetoskia.com/aetos/doc-weaver/releases/tag/0.1.0

186
README.md Normal file
View File

@@ -0,0 +1,186 @@
# doc-weaver
TypeScript documentation compiler that mirrors [`doc-forge`](../doc-forge).
`doc-weaver` reads **TypeScript + JSDoc** and emits documentation for both
humans and machines:
- **Humans** – a [TypeDoc](https://typedoc.org) site (the parallel to
doc-forge's MkDocs site).
- **Machines** – an MCP JSON bundle (`index.json`, `nav.json`, `modules/`)
whose shape is byte-compatible with the bundle `doc-forge` produces for
Python, so the same consumers (agents, `docs://` clients, the standards
registry) can query it unchanged.
Where `doc-forge` uses `mkdocs` + Google-style docstrings (`GSDFC`),
`doc-weaver` uses `TypeDoc` + Google-style JSDoc (`RDFC`).
---
## Parity map
| Concern | Python (`doc-forge`) | TypeScript (`doc-weaver`) |
| --- | --- | --- |
| Source of truth | Python docstrings (GSDFC) | TypeScript types + JSDoc (RDFC) |
| Front-end (loader) | Griffe static analysis | TypeDoc (TypeScript compiler API) |
| Middle-end (model) | `Project -> Module -> DocObject` | `Project -> Module -> DocObject` |
| Human renderer | MkDocs (`site/lib`) | TypeDoc (`site/lib`) |
| Machine renderer | `MCPRenderer` JSON | `TypeDocJsonToMCP` JSON |
| Convention | GSDFC (Google-style docstrings) | RDFC (Google-style JSDoc) |
| CLI | `doc-forge build/serve` | `doc-weaver build/serve` |
| MCP server | `docforge.servers.MCPServer` | `docweaver.servers.MCPServer` |
### MCP bundle (identical shape to doc-forge)
```
docs/mcp/
index.json { project, type: "docforge-model", modules_count, source }
nav.json [ { module, resource: "docs://modules/<module>" } ]
modules/<module>.json { module, content: { path, docstring, objects } }
```
Each object is rendered recursively as:
```
{ name, kind, path, signature, docstring, members? }
```
Resource URIs use the same `docs://` scheme as doc-forge:
- `docs://index`
- `docs://nav`
- `docs://modules/{module}`
Module paths use **dotted** form (`components.ResourceList`) so a single URI
segment matches, exactly like Python's dotted module paths.
---
## Installation
```bash
npm install
npm run build
```
This produces the `doc-weaver` binary in `dist/` and `bin/`.
---
## CLI usage
```bash
# Machine-readable MCP JSON
doc-weaver build --mcp --module src/index.ts --project-name my-app
# Human-facing TypeDoc site (MkDocs parallel)
doc-weaver build --docs --module src/index.ts --project-name my-app
# Both in one pass
doc-weaver build --mcp --docs --module src/index.ts
# Serve a generated MCP bundle
doc-weaver serve --mcp --module my-app --out-dir docs/mcp
```
Flags:
| Flag | Meaning | Default |
| --- | --- | --- |
| `--mcp` | Emit MCP JSON bundle | – |
| `--docs` / `--mkdocs` / `--lib` | Emit the TypeDoc human site | – |
| `--module <path>` / `--entry <file>` | Entry point for TypeDoc | required |
| `--project-name <name>` / `--name <name>` | Project name | `package.json` name |
| `--tsconfig <file>` | tsconfig to use | auto-discovered |
| `--out-dir <dir>` | MCP output directory | `docs/mcp` |
| `--out <dir>` | Human docs output directory | `site/lib` |
| `--plugin <name...>` | Extra TypeDoc plugins | – |
| `--strict` | Fail on TypeScript errors instead of generating anyway | off |
By default doc-weaver **skips TypeScript error checking** (`skipErrorChecking`),
so documentation is produced even when a project has unrelated type errors.
Pass `--strict` to make errors fatal.
`--api` and `--wiki` are reserved for parity with doc-forge and are not yet
implemented.
---
## Adding it to a React package
Add a script to the package's `package.json` (pointing at the package entry):
```jsonc
{
"scripts": {
"docs:mcp": "doc-weaver build --mcp --module index.ts --out-dir docs/mcp",
"docs:site": "doc-weaver build --docs --module index.ts --out site/lib",
"docs:serve": "doc-weaver serve --mcp --module my-app --out-dir docs/mcp"
}
}
```
Packages in this workspace (`react-openapi`, `react-auth`, `vet`) export their
public surface from `index.ts`, so a single `--module index.ts` documents the
package. To document **per-file modules**, pass a directory or several entry
points instead:
```bash
doc-weaver build --mcp --module src
doc-weaver build --mcp --module src/a.ts --module src/b.ts
```
---
## Architecture
```
TypeScript + JSDoc
|
TypeDoc (loader) -> serialized reflection JSON
|
TypeDocJsonToMCP (renderer) -> MCP bundle
generateHumanDocs (renderer)-> TypeDoc HTML site
```
```
src/
cli/ commands, mcp-utils
loaders/ typedoc-loader (static analysis)
models/ semantic model + TypeDoc helpers
renderers/ mcp-renderer, human-docs, docstring, signature
servers/ mcp-server (official MCP SDK)
templates/ typedoc.json baseline
```
Like doc-forge, `doc-weaver` is renderer-agnostic: the MCP renderer and the
TypeDoc renderer never depend on each other, and no user code is executed to
produce documentation semantics.
---
## Convention (RDFC)
See [`docs/wiki/02_rdfc.md`](docs/wiki/02_rdfc.md) for the *React Doc-forge
Convention* (RDFC) — Google-style JSDoc that keeps docstrings the single source
of truth and makes them machine-parsable.
The hand-written docs live under `docs/wiki/` and are built with MkDocs into
`site/wiki/` (mirroring doc-forge), while the API site is built by TypeDoc into
`site/lib/`:
```bash
# Wiki (MkDocs) -> site/wiki/
mkdocs build -f docs/mkdocs.wiki.yml
# API site (TypeDoc) + MCP bundle for doc-weaver itself
node bin/doc-weaver.js build --mcp --docs \
--module src --project-name doc-weaver \
--out-dir docs/mcp --out site/lib
```
---
## License
MIT

3
bin/doc-weaver.js Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import { main } from '../dist/cli/index.js';
main();

63
docs/mkdocs.wiki.yml Normal file
View File

@@ -0,0 +1,63 @@
theme:
name: material
palette:
scheme: slate
primary: blue grey
accent: teal
font:
text: Roboto
code: JetBrains Mono
features:
- navigation.sections
- navigation.expand
- navigation.top
- navigation.instant
- navigation.tracking
- navigation.indexes
- content.code.copy
- content.code.annotate
- content.tabs.link
- content.action.edit
- search.highlight
- search.share
- search.suggest
- navigation.tabs
- toc.integrate
- header.autohide
- content.code.select
- content.code.line_numbers
- content.tooltips
icon:
logo: material/file-document-multiple
repo: fontawesome/brands/github
markdown_extensions:
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.details
- pymdownx.highlight:
linenums: true
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- tables
- footnotes
- pymdownx.caret
- pymdownx.tilde
- pymdownx.mark
extra_css:
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
plugins:
- search
site_name: doc-weaver
docs_dir: wiki
site_dir: ../site/wiki
nav:
- Home: index.md
- Overview: 01_overview.md
- RDFC Guide: 02_rdfc.md

60
docs/wiki/01_overview.md Normal file
View File

@@ -0,0 +1,60 @@
# Library Overview
`doc-weaver` turns RDFC-compliant TypeScript JSDoc into maintainable reference
documentation. It never edits source comments; it reads them, renders them, and
assembles documentation from all available material — exactly as `doc-forge`
does for Python docstrings.
Analysis is static: source is parsed by the TypeScript compiler API via TypeDoc
and is never executed.
---
## 🧭 Which Build Should You Use?
| Flag | Source | Output |
|-----------------------|-----------------------|---------------------------------------------------|
| `--docs` (`--mkdocs`, `--lib`) | JSDoc comments | `site/lib/**` TypeDoc HTML (the MkDocs parallel) |
| `--mcp` | JSDoc comments + model | `docs/mcp/**` doc-forge-shaped JSON |
| `--api` | OpenAPI spec | reserved |
| `--wiki` | Hand-written markdown | `docs/wiki/**` (unchanged) |
---
## 🏗️ Architecture
```
TypeScript + JSDoc
|
TypeDoc (loader) -> serialized reflection JSON
|
TypeDocJsonToMCP (renderer) -> MCP bundle (docs/mcp/)
generateHumanDocs (renderer) -> TypeDoc HTML site (site/lib/)
```
The MCP renderer and the human renderer never depend on each other: both read
the same serialized reflections, so the machine bundle and the human site can
never disagree about a symbol.
---
## 🔄 MCP bundle shape
`docs/mcp/**` is a self-contained, machine-readable reference whose shape is
byte-compatible with `doc-forge`:
```
docs/mcp/
index.json { project, type: "docforge-model", modules_count, source }
nav.json [ { module, resource: "docs://modules/<module>" } ]
modules/<module>.json { module, content: { path, docstring, objects } }
```
`doc-weaver serve --mcp --module doc-weaver` serves it over stdio. See the
[RDFC Guide](02_rdfc.md) for how to write the JSDoc that fills it.
---
## ➡️ Read Next
- [02 – RDFC Guide](02_rdfc.md)

173
docs/wiki/02_rdfc.md Normal file
View File

@@ -0,0 +1,173 @@
# RDFC Guide
RDFC — the **React Doc-forge Convention** — is the TypeScript counterpart to
doc-forge's **GSDFC**. This page is the practical companion to the convention:
how to write JSDoc so it renders correctly in the TypeDoc site **and** stays
machine-parsable in the MCP bundle and by AI tooling.
---
## 🧭 Overview
- Documentation is the single source of truth and lives in JSDoc comments.
- `doc-weaver` compiles comments; it never rewrites or invents them.
- Structure follows the TypeScript export hierarchy.
- Every public export should have a complete and accurate JSDoc comment.
- Types live in signatures; do not duplicate them in prose unless a tag
requires it.
---
## 📦 Module (file) level
Document the file with a leading block comment. It becomes the module
`docstring` in the MCP bundle.
```ts
/**
* # Resource list
*
* Renders, filters, and paginates a resource collection.
*
* ---
*
* Notes:
* - Reads configuration from the OpenAPI spec.
* - Pagination is server-driven.
*/
```
---
## 📦 Components
Document the component function and its props. Components are classified
automatically as `kind: "component"` (PascalCase name).
```ts
/**
* Render a single resource row with actions.
*
* @param props - Component props.
* @param props.item - The resource record to display.
* @param props.onSelect - Invoked when the row is selected.
* @returns The rendered row element.
*
* @example
* ```tsx
* <ResourceList item={record} onSelect={handleSelect} />
* ```
*/
export function ResourceList(props: ListCellProps) {
// ...
}
```
---
## 📦 Hooks
Hooks are classified automatically as `kind: "hook"` (name starts with `use`).
```ts
/**
* Subscribe to server-sent events for an item.
*
* @param url - SSE endpoint, or `null` to disable.
* @param handlers - Event handlers keyed by event name.
* @returns Connection state and the latest payload.
*
* @remarks
* The subscription is torn down on unmount.
*/
export function useItemSse(url: string | null, handlers: ItemSseHandlers) {
// ...
}
```
---
## 📦 Types, interfaces, and members
Document the type and each member. Members become nested `members` entries in
the MCP bundle.
```ts
/**
* A single display field extracted from an OpenAPI schema.
*/
export interface FieldConfig {
/** Property name in the payload. */
name: string;
/** `x-edit-as: date` — edit a `date-time` field with a date input. */
editAs?: string;
/** Schema-level `default` used to seed create-form state. */
defaultValue?: unknown;
}
```
---
## 📦 Notes subsection grouping
Group related information using labeled subsections (Markdown bold):
```
Notes:
**Guarantees:**
- deterministic output
**Lifecycle:**
- created during initialization
- reused across renders
```
---
## 📦 Heading map
`doc-weaver` renders JSDoc block tags into stable markdown headings so both
TypeDoc and the MCP bundle agree:
| JSDoc tag | Rendered heading |
|-----------|------------------|
| `@param` | `**Parameters:**` |
| `@returns` / `@return` | `**Returns:**` |
| `@throws` | `**Throws:**` |
| `@example` | `**Example:**` |
| `@deprecated` | `**Deprecated:**` |
| `@see` | `**See also:**` |
| `@remarks` | `**Remarks:**` |
| *other* | `**@tag:**` |
---
## 📦 Mapping to output kinds
| TS construct | `kind` |
|--------------|--------|
| interface / type alias | `interface` / `type-alias` |
| function component (PascalCase) | `component` |
| custom hook (`use*`) | `hook` |
| plain function | `function` |
| object / class member | `property` / `method` |
| const / let / var | `variable` |
An explicit `@kind` tag overrides the inferred kind.
---
## 🛡️ Enforcement
Unlike doc-forge (which runs `pydoclint` in CI), `doc-weaver` performs no
signature linting: TypeDoc extracts whatever JSDoc is present. Keep comments
complete and accurate — they are the single source of truth for both the human
site and the machine bundle.
---
## Related
- [01 – Overview](01_overview.md)

55
docs/wiki/index.md Normal file
View File

@@ -0,0 +1,55 @@
# 🪡 doc-weaver — TypeScript Documentation Weaver
`doc-weaver` is the TypeScript counterpart to `doc-forge`. It compiles
JSDoc-annotated TypeScript into a **TypeDoc** site (the MkDocs parallel) and a
**doc-forge-shaped MCP JSON bundle**, so React packages document themselves for
humans and agents alike.
> **Doc model:** this wiki is written for humans — guides and conventions. The
> authoritative API contracts live in the code (JSDoc) and the machine-readable
> bundle under `docs/mcp/`.
---
## 🚀 Key Features
- 🧵 JSDoc extraction via TypeDoc (the TypeScript compiler API)
- 🧬 doc-forge-compatible MCP bundle (`index.json`, `nav.json`, `modules/*.json`)
- ⚛️ React-aware kinds — `component` (PascalCase) and `hook` (`use*`)
- 📚 RDFC (React Doc-forge Convention) — the JSDoc parallel to GSDFC
- 🔌 MCP server over stdio exposing `docs://index`, `docs://nav`, and
`docs://modules/{module}`
---
## 📦 Installation
```bash
npm install
npm run build
```
This produces the `doc-weaver` binary under `bin/` and `dist/`.
---
## 📁 Documentation Structure
| Section | Description |
|-----------------------------------------|------------------------------------------------|
| [Overview](01_overview.md) | What doc-weaver is and how it fits |
| [RDFC Guide](02_rdfc.md) | How to write RDFC-conformant JSDoc |
---
## 🔗 Related Resources
- **Source Code:** the `doc-weaver` repository
- **MCP Bundle:** generated by `doc-weaver build --mcp --module src/index.ts`
into `docs/mcp/`
- **Library Registry:** registered in the standards hub as `doc-weaver`
(MCP `:8010`)
---
© Aetoskia Internal — `doc-weaver` 0.1.0

1903
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "@aetoskia/doc-weaver",
"version": "0.1.0",
"type": "module",
"description": "TypeScript documentation compiler using TypeDoc + JSDoc (Google-style), generating MkDocs and MCP JSON in doc-forge shape.",
"bin": {
"doc-weaver": "./bin/doc-weaver.js"
},
"files": [
"bin",
"dist",
"README.md",
"CHANGELOG.md"
],
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.json",
"quality-gate": "tsc -p tsconfig.json --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit",
"prepare": "tsc -p tsconfig.json",
"dev": "tsx src/cli/index.ts",
"start": "node bin/doc-weaver.js"
},
"publishConfig": {
"registry": "https://npm.aetoskia.com"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"commander": "^12.1.0",
"fs-extra": "^11.3.1",
"typedoc": "^0.27.9"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.19.11",
"tsx": "^4.20.5",
"typescript": "~5.6.3"
}
}

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);
}

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

@@ -0,0 +1,156 @@
import { ReflectionKind } from 'typedoc';
/** A single documented parameter (name + rendered description). */
export interface ParamDoc {
name: string;
description: string;
}
/**
* 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). It moves `@param`/`@typeParam`
* descriptions onto the signature's parameters, so those are passed in
* separately as `params`/`typeParams` and rendered as grouped sections.
*
* The result is a stable markdown string so both MkDocs and MCP consumers
* receive the same human-readable documentation.
*/
export function renderComment(
comment: any,
params?: ParamDoc[],
typeParams?: ParamDoc[]
): string {
const documentedParams = (params || []).filter((p) => (p.description || '').trim());
const documentedTypeParams = (typeParams || []).filter((p) => (p.description || '').trim());
if (!comment && !documentedParams.length && !documentedTypeParams.length) return '';
const lines: string[] = [];
const summary = renderParts(comment?.summary);
if (summary.trim()) lines.push(summary.trim());
if (documentedTypeParams.length) {
lines.push('');
lines.push('**Type Parameters:**');
lines.push('');
for (const p of documentedTypeParams) {
lines.push(`- \`${p.name}\` — ${oneLine(p.description)}`);
}
}
if (documentedParams.length) {
lines.push('');
lines.push('**Parameters:**');
lines.push('');
for (const p of documentedParams) {
lines.push(`- \`${p.name}\` — ${oneLine(p.description)}`);
}
}
for (const tag of comment?.blockTags || []) {
const tagName = (tag.tag || '').replace(/^@/, '');
// @param / @typeParam are rendered from signature parameters above.
if (tagName === 'param' || tagName === 'typeParam' || tagName === 'template') continue;
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();
}
/** Collapse a possibly multi-line description to a single line. */
function oneLine(text: string): string {
return (text || '').trim().replace(/\s*\n\s*/g, ' ');
}
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,178 @@
import {
RefKind,
reflectionKindName,
resolveModulePath,
collectModules
} from '../models/typedoc.js';
import { renderComment, renderParts, mapKind, classifyReact, ParamDoc } 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> = {};
const children = (mod.children || []).filter((c: any) => c.kind !== RefKind.Module);
const siblingNames = new Set(children.map((c: any) => c.name));
const baseName = moduleBaseName(modPath);
for (const child of children) {
// TypeDoc names default exports "default"; surface them under the
// module's own name (a React component/module convention) unless a
// sibling already claims that name.
const name = child.name === 'default' && !siblingNames.has(baseName) ? baseName : child.name;
objects[name] = this.renderObject(child, modPath, name);
}
return {
path: modPath,
docstring: renderComment(mod.comment),
objects
};
}
private renderObject(ref: any, parentPath: string, nameOverride?: string): Record<string, unknown> {
const name = nameOverride ?? ref.name;
const kindName = classifyReact(name, mapKind(ref.kind));
const path = `${parentPath}.${name}`;
const data: Record<string, unknown> = {
name,
kind: kindName,
path,
signature: bestSignature(ref),
docstring: this.renderDocstring(ref)
};
// 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;
}
/**
* Render a reflection's docstring, sourcing the summary/blockTags from the
* signature when TypeDoc has moved them there (functions, method-like
* type aliases) and attaching parameter documentation.
*/
private renderDocstring(ref: any): string {
const sig = bestSignatureRef(ref);
const refComment = ref.comment;
const sigComment = sig?.comment;
const params: ParamDoc[] = (sig?.parameters || [])
.map((p: any) => ({ name: p.name, description: renderParts(p.comment?.summary) }))
.filter((p: ParamDoc) => (p.description || '').trim().length > 0);
const typeParams: ParamDoc[] = (sig?.typeParameters || [])
.map((p: any) => ({ name: p.name, description: renderParts(p.comment?.summary) }))
.filter((p: ParamDoc) => (p.description || '').trim().length > 0);
const summary =
refComment?.summary && refComment.summary.length
? refComment.summary
: sigComment?.summary;
const blockTags = dedupeTags([
...(refComment?.blockTags || []),
...(sigComment?.blockTags || [])
]);
return renderComment({ summary, blockTags }, params, typeParams);
}
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;
}
/** Last segment of a (dotted) module path, used to name default exports. */
function moduleBaseName(modPath: string): string {
const parts = modPath.split('.');
return parts[parts.length - 1] || modPath;
}
/** Find the signature TypeDoc attached comment data to, if any. */
function bestSignatureRef(ref: any): any {
if (ref?.signatures?.length) return ref.signatures[0];
const decl = ref?.type?.declaration;
if (decl?.signatures?.length) return decl.signatures[0];
return undefined;
}
/** Remove duplicate block tags (same tag + same content) across sources. */
function dedupeTags(tags: any[]): any[] {
const seen = new Set<string>();
const out: any[] = [];
for (const tag of tags) {
const key = `${tag?.tag || ''}|${(tag?.content || []).map((p: any) => p.text ?? '').join('')}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(tag);
}
return out;
}

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);
}
}

31
templates/typedoc.json Normal file
View File

@@ -0,0 +1,31 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPointStrategy": "resolve",
"entryPoints": ["index.ts"],
"readme": "none",
"excludePrivate": false,
"excludeInternal": false,
"categorizeByGroup": true,
"navigation": {
"includeCategories": true,
"includeGroups": true
},
"plugin": [],
"commentStyle": "jsdoc",
"blockTags": [
"@defaultValue",
"@deprecated",
"@example",
"@param",
"@remarks",
"@returns",
"@see",
"@throws"
],
"inlineTags": ["@link", "@linkcode", "@linkplain"],
"validation": {
"notExported": false,
"invalidLink": true,
"notDocumented": false
}
}

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "templates"]
}