Render signature-level parameter docs and surface default exports in MCP bundles
This commit is contained in:
@@ -1,22 +1,58 @@
|
||||
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, @param). This function renders
|
||||
* them back to a stable markdown string so both MkDocs and MCP consumers
|
||||
* 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): string {
|
||||
if (!comment) return '';
|
||||
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);
|
||||
const summary = renderParts(comment?.summary);
|
||||
if (summary.trim()) lines.push(summary.trim());
|
||||
|
||||
for (const tag of comment.blockTags || []) {
|
||||
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);
|
||||
@@ -29,6 +65,11 @@ export function renderComment(comment: any): string {
|
||||
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':
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolveModulePath,
|
||||
collectModules
|
||||
} from '../models/typedoc.js';
|
||||
import { renderComment, mapKind, classifyReact } from './docstring.js';
|
||||
import { renderComment, renderParts, mapKind, classifyReact, ParamDoc } from './docstring.js';
|
||||
import { bestSignature } from './signature.js';
|
||||
|
||||
export interface McpBundle {
|
||||
@@ -60,9 +60,15 @@ export class TypeDocJsonToMCP {
|
||||
|
||||
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);
|
||||
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,
|
||||
@@ -71,16 +77,17 @@ export class TypeDocJsonToMCP {
|
||||
};
|
||||
}
|
||||
|
||||
private renderObject(ref: any, parentPath: string): Record<string, unknown> {
|
||||
const kindName = classifyReact(ref.name, mapKind(ref.kind));
|
||||
const path = `${parentPath}.${ref.name}`;
|
||||
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: ref.name,
|
||||
name,
|
||||
kind: kindName,
|
||||
path,
|
||||
signature: bestSignature(ref),
|
||||
docstring: renderComment(ref.comment)
|
||||
docstring: this.renderDocstring(ref)
|
||||
};
|
||||
|
||||
// Prefer an explicit @kind tag when present (mirrors doc-forge kinds).
|
||||
@@ -93,6 +100,37 @@ export class TypeDocJsonToMCP {
|
||||
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 || []) {
|
||||
@@ -111,3 +149,30 @@ function findBlockTag(comment: any, tag: string): string | undefined {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user