Skip to content

Commit 0cd75bb

Browse files
Royal-lobsterclaude
andcommitted
feat: standardize README structure and add auto-sync workflow
- Restructure README to match mcp-opinion template with badges, usage examples, and cleaner organization - Add sync-tools.yml workflow to auto-generate tool documentation - Add generate-tools.mjs action that handles array-based tool exports - Add zod-to-json-schema devDependency for tool schema conversion - Include AUTO-GENERATED TOOLS markers for automated documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6b3fd1e commit 0cd75bb

File tree

4 files changed

+406
-181
lines changed

4 files changed

+406
-181
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { zodToJsonSchema } from "zod-to-json-schema";
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
const ROOT = path.resolve(__dirname, "../../..");
8+
const README_PATH = path.join(ROOT, "README.md");
9+
const TOOLS_DIR = path.join(ROOT, "src", "tools");
10+
11+
const START = "<!-- AUTO-GENERATED TOOLS START -->";
12+
const END = "<!-- AUTO-GENERATED TOOLS END -->";
13+
14+
/**
15+
* Check if value is an MCP tool object
16+
*/
17+
function isMcpTool(exp) {
18+
return (
19+
exp &&
20+
typeof exp === "object" &&
21+
typeof exp.name === "string" &&
22+
typeof exp.description === "string" &&
23+
(exp.parameters || exp.schema)
24+
);
25+
}
26+
27+
/**
28+
* Load MCP tools from the tools directory
29+
* Supports two patterns:
30+
* 1. Array export (e.g., defillamaTools = [...]) in index.ts
31+
* 2. Individual tool exports from separate files (excluding index.ts)
32+
*/
33+
async function loadTools() {
34+
const tools = [];
35+
36+
// First, try to load tools from index.ts (array pattern)
37+
const indexPath = path.join(TOOLS_DIR, "index.ts");
38+
if (fs.existsSync(indexPath)) {
39+
try {
40+
const mod = await import(indexPath);
41+
42+
// Look for array exports containing tool objects
43+
for (const [key, value] of Object.entries(mod)) {
44+
if (Array.isArray(value)) {
45+
const arrayTools = value.filter(isMcpTool);
46+
if (arrayTools.length > 0) {
47+
console.log(
48+
`Found ${arrayTools.length} tools in array export '${key}'`,
49+
);
50+
tools.push(...arrayTools);
51+
}
52+
} else if (isMcpTool(value)) {
53+
tools.push(value);
54+
}
55+
}
56+
} catch (err) {
57+
console.warn(`Warning: Could not load index.ts: ${err.message}`);
58+
}
59+
}
60+
61+
// If no tools found in index.ts, try individual files
62+
if (tools.length === 0) {
63+
const files = fs
64+
.readdirSync(TOOLS_DIR)
65+
.filter((f) => f.endsWith(".ts") && f !== "index.ts");
66+
67+
const toolPromises = files.map(async (file) => {
68+
try {
69+
const mod = await import(path.join(TOOLS_DIR, file));
70+
71+
const matches = Object.values(mod).filter(isMcpTool);
72+
73+
if (matches.length === 0) {
74+
return null;
75+
}
76+
77+
if (matches.length > 1) {
78+
console.warn(
79+
`Warning: ${file} exports multiple MCP-like tools. Using the first one.`,
80+
);
81+
}
82+
83+
return matches[0];
84+
} catch (err) {
85+
console.warn(`Warning: Could not load ${file}: ${err.message}`);
86+
return null;
87+
}
88+
});
89+
90+
const loadedTools = await Promise.all(toolPromises);
91+
tools.push(...loadedTools.filter(Boolean));
92+
}
93+
94+
return tools.sort((a, b) => a.name.localeCompare(b.name));
95+
}
96+
97+
function renderSchema(schema) {
98+
if (!schema) {
99+
return "_No parameters_";
100+
}
101+
102+
// If this is a Zod schema, convert it to JSON Schema
103+
const jsonSchema =
104+
typeof schema.safeParse === "function" ? zodToJsonSchema(schema) : schema;
105+
106+
const properties = jsonSchema.properties ?? {};
107+
const required = new Set(jsonSchema.required ?? []);
108+
109+
if (Object.keys(properties).length === 0) {
110+
return "_No parameters_";
111+
}
112+
113+
// Filter out internal parameters (starting with _)
114+
const publicProperties = Object.entries(properties).filter(
115+
([key]) => !key.startsWith("_"),
116+
);
117+
118+
if (publicProperties.length === 0) {
119+
return "_No parameters_";
120+
}
121+
122+
// Check if any param has a default value to determine table columns
123+
const hasDefaults = publicProperties.some(
124+
([, prop]) => prop.default !== undefined,
125+
);
126+
127+
// Build table header
128+
let table = hasDefaults
129+
? "| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n"
130+
: "| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n";
131+
132+
// Build table rows
133+
for (const [key, prop] of publicProperties) {
134+
const type = Array.isArray(prop.type)
135+
? prop.type.join(" | ")
136+
: (prop.type ?? "unknown");
137+
138+
const requiredStr = required.has(key) ? "Yes" : "No";
139+
const description = prop.description ?? "";
140+
const defaultVal =
141+
prop.default !== undefined ? JSON.stringify(prop.default) : "";
142+
143+
if (hasDefaults) {
144+
table += `| \`${key}\` | ${type} | ${requiredStr} | ${defaultVal} | ${description} |\n`;
145+
} else {
146+
table += `| \`${key}\` | ${type} | ${requiredStr} | ${description} |\n`;
147+
}
148+
}
149+
150+
return table.trim();
151+
}
152+
153+
function renderMarkdown(tools) {
154+
let md = "";
155+
156+
for (const tool of tools) {
157+
const schema = tool.parameters || tool.schema;
158+
159+
md += `### \`${tool.name}\`\n`;
160+
md += `${tool.description}\n\n`;
161+
md += `${renderSchema(schema)}\n\n`;
162+
}
163+
164+
return md.trim();
165+
}
166+
167+
function updateReadme({ readme, tools }) {
168+
if (!readme.includes(START) || !readme.includes(END)) {
169+
throw new Error("README missing AUTO-GENERATED TOOLS markers");
170+
}
171+
172+
const toolsMd = renderMarkdown(tools);
173+
174+
return readme.replace(
175+
new RegExp(`${START}[\\s\\S]*?${END}`, "m"),
176+
`${START}\n\n${toolsMd}\n\n${END}`,
177+
);
178+
}
179+
180+
async function main() {
181+
try {
182+
const readme = fs.readFileSync(README_PATH, "utf8");
183+
const tools = await loadTools();
184+
185+
if (tools.length === 0) {
186+
console.warn("Warning: No tools found!");
187+
}
188+
189+
const updated = updateReadme({ readme, tools });
190+
191+
fs.writeFileSync(README_PATH, updated);
192+
console.log(`Synced ${tools.length} MCP tools to README.md`);
193+
} catch (error) {
194+
console.error("Error updating README:", error);
195+
process.exit(1);
196+
}
197+
}
198+
199+
main().catch((err) => {
200+
console.error(err);
201+
process.exit(1);
202+
});

.github/workflows/sync-tools.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Sync MCP Tool Docs
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
sync-tools:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout repo
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Node
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 20
23+
24+
- name: Install dependencies
25+
run: |
26+
if [ -f pnpm-lock.yaml ]; then
27+
corepack enable
28+
pnpm install --frozen-lockfile
29+
else
30+
npm install
31+
fi
32+
33+
- name: Generate MCP tool documentation
34+
run: npx tsx .github/actions/generate-mcp-tools/generate-tools.mjs
35+
36+
- name: Commit README changes
37+
run: |
38+
if git diff --quiet; then
39+
echo "No README changes"
40+
exit 0
41+
fi
42+
43+
git config --local user.email "action@github.com"
44+
git config --local user.name "GitHub Action"
45+
git add README.md
46+
git commit -m "chore: auto-sync MCP tool documentation"
47+
git push

0 commit comments

Comments
 (0)