Skip to content

Commit 7d89fa4

Browse files
feat: standardize README and add auto-sync workflows (#10)
- Update README structure to match standardized format with badges, overview, features, installation, usage examples, and development sections - Add auto-generated tools markers for MCP tool documentation - Add sync-tools.yml workflow to auto-generate tool documentation - Update push.yml and release.yml to template format - Add generate-mcp-tools action for README tool sync - Add zod-to-json-schema dependency for tool documentation Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 33fe304 commit 7d89fa4

File tree

7 files changed

+296
-166
lines changed

7 files changed

+296
-166
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
* Load MCP tools
16+
* Scans for any export that looks like an MCP tool:
17+
* {
18+
* name: string,
19+
* description: string,
20+
* parameters?: ZodSchema
21+
* schema?: JSONSchema
22+
* }
23+
*/
24+
async function loadTools() {
25+
const files = fs
26+
.readdirSync(TOOLS_DIR)
27+
.filter((f) => f.endsWith(".ts") && f !== "index.ts");
28+
29+
const toolPromises = files.map(async (file) => {
30+
const mod = await import(path.join(TOOLS_DIR, file));
31+
32+
const matches = Object.values(mod).filter(
33+
(exp) =>
34+
exp &&
35+
typeof exp === "object" &&
36+
typeof exp.name === "string" &&
37+
typeof exp.description === "string" &&
38+
(exp.parameters || exp.schema),
39+
);
40+
41+
if (matches.length === 0) {
42+
return null;
43+
}
44+
45+
if (matches.length > 1) {
46+
console.warn(
47+
`Warning: ${file} exports multiple MCP-like tools. Using the first one.`,
48+
);
49+
}
50+
51+
return matches[0];
52+
});
53+
54+
const loadedTools = await Promise.all(toolPromises);
55+
const tools = loadedTools.filter(Boolean);
56+
57+
return tools.sort((a, b) => a.name.localeCompare(b.name));
58+
}
59+
60+
function renderSchema(schema) {
61+
if (!schema) {
62+
return "_No parameters_";
63+
}
64+
65+
// If this is a Zod schema, convert it to JSON Schema
66+
const jsonSchema =
67+
typeof schema.safeParse === "function" ? zodToJsonSchema(schema) : schema;
68+
69+
const properties = jsonSchema.properties ?? {};
70+
const required = new Set(jsonSchema.required ?? []);
71+
72+
if (Object.keys(properties).length === 0) {
73+
return "_No parameters_";
74+
}
75+
76+
// Check if any param has a default value to determine table columns
77+
const hasDefaults = Object.values(properties).some(
78+
(prop) => prop.default !== undefined,
79+
);
80+
81+
// Build table header
82+
let table = hasDefaults
83+
? "| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n"
84+
: "| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n";
85+
86+
// Build table rows
87+
for (const [key, prop] of Object.entries(properties)) {
88+
const type = Array.isArray(prop.type)
89+
? prop.type.join(" | ")
90+
: (prop.type ?? "unknown");
91+
92+
const requiredStr = required.has(key) ? "✅" : "";
93+
const description = prop.description ?? "";
94+
const defaultVal =
95+
prop.default !== undefined ? JSON.stringify(prop.default) : "";
96+
97+
if (hasDefaults) {
98+
table += `| \`${key}\` | ${type} | ${requiredStr} | ${defaultVal} | ${description} |\n`;
99+
} else {
100+
table += `| \`${key}\` | ${type} | ${requiredStr} | ${description} |\n`;
101+
}
102+
}
103+
104+
return table.trim();
105+
}
106+
107+
function renderMarkdown(tools) {
108+
let md = "";
109+
110+
for (const tool of tools) {
111+
const schema = tool.parameters || tool.schema;
112+
113+
md += `### \`${tool.name}\`\n`;
114+
md += `${tool.description}\n\n`;
115+
md += `${renderSchema(schema)}\n\n`;
116+
}
117+
118+
return md.trim();
119+
}
120+
121+
function updateReadme({ readme, tools }) {
122+
if (!readme.includes(START) || !readme.includes(END)) {
123+
throw new Error("README missing AUTO-GENERATED TOOLS markers");
124+
}
125+
126+
const toolsMd = renderMarkdown(tools);
127+
128+
return readme.replace(
129+
new RegExp(`${START}[\\s\\S]*?${END}`, "m"),
130+
`${START}\n\n${toolsMd}\n\n${END}`,
131+
);
132+
}
133+
134+
async function main() {
135+
try {
136+
const readme = fs.readFileSync(README_PATH, "utf8");
137+
const tools = await loadTools();
138+
139+
if (tools.length === 0) {
140+
console.warn("Warning: No tools found!");
141+
}
142+
143+
const updated = updateReadme({ readme, tools });
144+
145+
fs.writeFileSync(README_PATH, updated);
146+
console.log(`Synced ${tools.length} MCP tools to README.md`);
147+
} catch (error) {
148+
console.error("Error updating README:", error);
149+
process.exit(1);
150+
}
151+
}
152+
153+
main().catch((err) => {
154+
console.error(err);
155+
process.exit(1);
156+
});

.github/workflows/push.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
name: Push checks
2-
on: [push]
1+
name: Push checks (Template - Manual Trigger)
2+
3+
# To enable, change to e.g.: on: [push]
4+
on: workflow_dispatch
35

46
jobs:
57
Checkout:
@@ -35,5 +37,3 @@ jobs:
3537
run: pnpm run build
3638
- name: Biome Lint Check
3739
run: pnpm run lint
38-
- name: Tests
39-
run: pnpm test

.github/workflows/release.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
name: Release
1+
name: Release (Template - Manual Trigger)
22

3-
on:
4-
push:
5-
branches:
6-
- main
3+
# To enable, change to e.g.:
4+
# on:
5+
# push:
6+
# branches:
7+
# - main
8+
on: workflow_dispatch
79

810
concurrency: ${{ github.workflow }}-${{ github.ref }}
911

@@ -48,4 +50,4 @@ jobs:
4850
publish: pnpm run publish-packages
4951
env:
5052
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.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)