@lifeaitools/rdc-skills 0.24.42 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1518 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +375 -376
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +150 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +145 -151
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +57 -57
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +482 -482
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +502 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +538 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +167 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +203 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -236
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +160 -219
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/onramp/SKILL.md +248 -0
  131. package/skills/overnight/SKILL.md +251 -251
  132. package/skills/plan/SKILL.md +345 -345
  133. package/skills/preplan/SKILL.md +90 -90
  134. package/skills/prototype/SKILL.md +150 -150
  135. package/skills/rdc-brochurify/SKILL.md +245 -245
  136. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  137. package/skills/release/SKILL.md +140 -140
  138. package/skills/report/SKILL.md +100 -100
  139. package/skills/review/SKILL.md +152 -152
  140. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  141. package/skills/rpms-filemap/SKILL.md +111 -111
  142. package/skills/self-test/SKILL.md +132 -132
  143. package/skills/status/SKILL.md +99 -99
  144. package/skills/terminal-config/SKILL.md +62 -62
  145. package/skills/tests/MATRIX.md +54 -54
  146. package/skills/tests/README.md +47 -47
  147. package/skills/tests/onramp.test.json +87 -0
  148. package/skills/tests/rdc-brochure.test.json +34 -34
  149. package/skills/tests/rdc-build.test.json +36 -36
  150. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  151. package/skills/tests/rdc-co-develop.test.json +29 -29
  152. package/skills/tests/rdc-collab.test.json +29 -29
  153. package/skills/tests/rdc-convert.test.json +35 -35
  154. package/skills/tests/rdc-deploy.test.json +30 -30
  155. package/skills/tests/rdc-design.test.json +27 -27
  156. package/skills/tests/rdc-edit.test.json +29 -29
  157. package/skills/tests/rdc-fixit.test.json +36 -36
  158. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  159. package/skills/tests/rdc-handoff.test.json +28 -28
  160. package/skills/tests/rdc-help.test.json +29 -29
  161. package/skills/tests/rdc-housekeeping.test.json +28 -32
  162. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  163. package/skills/tests/rdc-overnight.test.json +37 -37
  164. package/skills/tests/rdc-plan.test.json +27 -27
  165. package/skills/tests/rdc-preplan.test.json +31 -31
  166. package/skills/tests/rdc-prototype.test.json +28 -28
  167. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  168. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  169. package/skills/tests/rdc-release.test.json +29 -29
  170. package/skills/tests/rdc-report.test.json +28 -28
  171. package/skills/tests/rdc-review.test.json +29 -29
  172. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  173. package/skills/tests/rdc-self-test.test.json +24 -24
  174. package/skills/tests/rdc-status.test.json +29 -29
  175. package/skills/tests/rdc-terminal-config.test.json +29 -29
  176. package/skills/tests/rdc-watch.test.json +24 -24
  177. package/skills/tests/rdc-workitems.test.json +27 -27
  178. package/skills/watch/SKILL.md +97 -97
  179. package/skills/workitems/SKILL.md +151 -151
  180. package/tests/acceptance.test.mjs +59 -59
  181. package/tests/channel-formatter.contract.test.mjs +251 -251
  182. package/tests/curl-surface.test.mjs +289 -289
  183. package/tests/harness-gates.test.mjs +325 -325
  184. package/tests/help-surface.test.mjs +61 -61
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/manifest-contract-fields.test.mjs +78 -78
  187. package/tests/mcp.test.mjs +271 -271
  188. package/tests/rdc-brochure.test.mjs +125 -125
  189. package/tests/require-work-item-on-commit.test.mjs +162 -162
  190. package/tests/run-evidence-gate.test.mjs +82 -82
  191. package/tests/skill-test-matrix.test.mjs +66 -66
  192. package/tests/validate-skills.js +27 -27
  193. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  194. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
  195. package/RELEASE.md +0 -42
  196. package/tests/housekeeping-lessons-triage.test.mjs +0 -49
  197. package/tests/lessons-pipeline-contract.test.mjs +0 -27
  198. package/tests/release-contract.test.mjs +0 -16
@@ -1,316 +1,316 @@
1
- #!/usr/bin/env node
2
- /**
3
- * bin/rdc-skills-mcp.mjs — local MCP server exposing the rdc-skills library.
4
- *
5
- * Mirrors the codeflow MCP topology (packages/codeflow/src/mcp/server.ts):
6
- * express + @modelcontextprotocol/sdk McpServer + StreamableHTTPServerTransport,
7
- * stateless (a fresh server+transport per POST /mcp), PORT from env.
8
- *
9
- * Routes:
10
- * POST /mcp — MCP over StreamableHTTP. NO Authorization (URL is the shared
11
- * secret, consistent with codeflow/clauth tunnel MCPs).
12
- * GET /health — { status, service, version, skills } — no auth, no heavy work.
13
- * /.well-known/* + /authorize + /token → 404 (force connectors to skip OAuth).
14
- *
15
- * It is tunneled in production at https://rdc-skills.regendevcorp.com/mcp; this
16
- * process only listens on PORT (default 3110) and does not configure the tunnel.
17
- *
18
- * ── Caller detection → variant (best-effort) ────────────────────────────────
19
- * Tools render a `cli` or `cloud` body. Detection rule: on MCP `initialize`,
20
- * `clientInfo.name` containing `claude-code` or `codex` → `cli`; anything else
21
- * (claude.ai web) → `cloud`; unknown → `cloud` (safer for web).
22
- *
23
- * APPROACH TAKEN — default + explicit override (NOT session-threaded):
24
- * The transport is stateless (a new McpServer per POST, sessionIdGenerator:
25
- * undefined), so there is no durable session to thread clientInfo through to a
26
- * later tools/call. Rather than build a fragile session map, we:
27
- * - capture clientInfo on `initialize` and remember the MOST RECENT one
28
- * process-wide as a soft default (helps the common single-client case), and
29
- * - ALWAYS honor an explicit `variant` arg on rdc_skill_get (the must-have).
30
- * If no explicit variant is given and no client has initialized this process,
31
- * the default is `cloud`. This keeps the override correct and the auto-detect
32
- * best-effort, exactly as the spec permits.
33
- */
34
-
35
- import express from 'express';
36
- import { z } from 'zod';
37
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
38
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
39
- import fs from 'node:fs';
40
- import path from 'node:path';
41
- import { fileURLToPath } from 'node:url';
42
- import { execFileSync } from 'node:child_process';
43
-
44
- import {
45
- listSkills,
46
- getSkill,
47
- getSkillBody,
48
- getCloudOverride,
49
- resolveSkillName,
50
- searchSkills,
51
- } from '../lib/catalog.mjs';
52
- import { toCloudBody } from '../lib/cloud-rewrite.mjs';
53
-
54
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
55
- const REPO_ROOT = path.resolve(__dirname, '..');
56
- const PORT = parseInt(process.env.PORT || '3110', 10);
57
-
58
- function pkgVersion() {
59
- try {
60
- return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version || '0.0.0';
61
- } catch {
62
- return '0.0.0';
63
- }
64
- }
65
-
66
- // Commit the running process actually LOADED — resolved ONCE at startup, never
67
- // per-request, so /health reports a PROVABLE commit. Resolution order:
68
- // 1. git-sha.json — baked at pack/publish (scripts/stamp-git-sha.mjs). This is
69
- // the PRODUCTION path: the process runs from the npm install (no .git), so
70
- // runtime rev-parse can't work — the stamped file is the source of truth.
71
- // 2. runtime `git rev-parse` — the DEV path (running straight from the checkout).
72
- // 3. env override, then 'unknown'.
73
- const GIT_SHA = (() => {
74
- try {
75
- const stamped = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'git-sha.json'), 'utf8')).sha;
76
- if (stamped && stamped !== 'unknown') return stamped;
77
- } catch { /* not stamped — fall through to runtime/dev resolution */ }
78
- try {
79
- return execFileSync('git', ['rev-parse', 'HEAD'], {
80
- cwd: REPO_ROOT,
81
- encoding: 'utf8',
82
- stdio: ['ignore', 'pipe', 'ignore'],
83
- }).trim();
84
- } catch {
85
- return process.env.RDC_SKILLS_GIT_SHA || 'unknown';
86
- }
87
- })();
88
-
89
- // Soft process-wide default variant, updated whenever a client initializes.
90
- let lastDetectedVariant = 'cloud';
91
-
92
- /** Map a clientInfo.name → variant. */
93
- function variantForClient(clientName) {
94
- const n = String(clientName || '').toLowerCase();
95
- if (n.includes('claude-code') || n.includes('codex')) return 'cli';
96
- return 'cloud'; // claude.ai web client and anything unknown
97
- }
98
-
99
- /**
100
- * Render a skill body for the resolved variant.
101
- * - cli → SKILL.md body unchanged.
102
- * - cloud → SKILL.cloud.md verbatim if present, else toCloudBody(SKILL.md).
103
- * Returns { header, body } or null if the skill has no body on disk.
104
- */
105
- function renderSkill(name, variant) {
106
- const body = getSkillBody(name);
107
- if (body == null) return null;
108
- if (variant === 'cli') {
109
- return { header: `<!-- rdc-skills: '${name}' served as CLI variant -->`, body };
110
- }
111
- const override = getCloudOverride(name);
112
- if (override != null) {
113
- return {
114
- header: `<!-- rdc-skills: '${name}' served as CLOUD variant (hand-tuned SKILL.cloud.md) -->`,
115
- body: override,
116
- };
117
- }
118
- return {
119
- header: `<!-- rdc-skills: '${name}' served as CLOUD variant (auto-rewritten from SKILL.md) -->`,
120
- body: toCloudBody(body),
121
- };
122
- }
123
-
124
- function textResult(text) {
125
- return { content: [{ type: 'text', text }] };
126
- }
127
-
128
- function directMcpUsage(message) {
129
- return {
130
- jsonrpc: '2.0',
131
- error: {
132
- code: -32000,
133
- message,
134
- help: {
135
- endpoint: '/mcp',
136
- method: 'POST',
137
- headers: {
138
- 'Content-Type': 'application/json',
139
- Accept: 'application/json, text/event-stream',
140
- },
141
- tools: ['rdc_skill_list', 'rdc_skill_search', 'rdc_skill_get'],
142
- response: 'Streamable HTTP returns Server-Sent Events. Parse the JSON-RPC envelope from each data: line; tool text is at result.content[0].text.',
143
- curl: `curl -s -X POST https://rdc-skills.regendevcorp.com/mcp \\
144
- -H 'Content-Type: application/json' \\
145
- -H 'Accept: application/json, text/event-stream' \\
146
- -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"rdc_skill_list","arguments":{}}}'`,
147
- },
148
- },
149
- id: null,
150
- };
151
- }
152
-
153
- function acceptsStreamableHttp(req) {
154
- const accept = String(req.headers.accept || '');
155
- return accept.includes('application/json') && accept.includes('text/event-stream');
156
- }
157
-
158
- function buildMcpServer() {
159
- const srv = new McpServer({ name: 'rdc-skills', version: pkgVersion() });
160
-
161
- // Capture clientInfo on initialize to refine the soft default variant.
162
- // The SDK exposes the underlying low-level server; oninitialized fires after
163
- // the client's initialize params are recorded.
164
- try {
165
- srv.server.oninitialized = () => {
166
- const info = srv.server.getClientVersion?.();
167
- if (info?.name) lastDetectedVariant = variantForClient(info.name);
168
- };
169
- } catch {
170
- /* non-fatal: detection stays at the cloud default */
171
- }
172
-
173
- // ── rdc_skill_list ─────────────────────────────────────────────────────────
174
- srv.registerTool(
175
- 'rdc_skill_list',
176
- {
177
- description:
178
- 'List the rdc-skills catalog: every skill with its slash form, category, summary, when-to-use triggers, usage, args, and required capabilities. No input required.',
179
- inputSchema: {},
180
- },
181
- async () => {
182
- const catalog = listSkills();
183
- return textResult(JSON.stringify({ count: catalog.length, skills: catalog }, null, 2));
184
- },
185
- );
186
-
187
- // ── rdc_skill_get ──────────────────────────────────────────────────────────
188
- srv.registerTool(
189
- 'rdc_skill_get',
190
- {
191
- description:
192
- "Get a skill's SKILL.md body rendered for the caller. variant 'cli' returns the body unchanged; 'cloud' rewrites local-shell/clauth-daemon steps for the claude.ai web client. Omit variant to use auto-detection (defaults to cloud). format 'json' returns metadata plus rendered body for direct API/curl callers.",
193
- inputSchema: {
194
- name: z.string().describe('Skill name or slash form (e.g. "deploy", "rdc:build", "rdc:brochurify", "lifeai-brochure-author"). See rdc_skill_list.'),
195
- variant: z.enum(['cli', 'cloud']).optional().describe('Force the rendered variant; overrides caller detection.'),
196
- format: z.enum(['text', 'json']).optional().describe("Return 'text' (default) for agent-readable SKILL.md, or 'json' for metadata plus rendered body."),
197
- },
198
- },
199
- async ({ name, variant, format }) => {
200
- const resolvedName = resolveSkillName(name);
201
- if (!resolvedName) {
202
- const valid = listSkills().map((s) => ({
203
- name: s.name,
204
- slash: s.slash,
205
- aliases: s.aliases,
206
- usage: s.usage,
207
- }));
208
- if (format === 'json') {
209
- return textResult(JSON.stringify({
210
- error: 'unknown_skill',
211
- requested: name,
212
- message: `Unknown skill '${name}'.`,
213
- valid_count: valid.length,
214
- valid,
215
- }, null, 2));
216
- }
217
- return textResult(`Unknown skill '${name}'. Valid skills (${valid.length}): ${valid.map((s) => `${s.name} (${s.slash})`).join(', ')}`);
218
- }
219
- const resolved = variant || lastDetectedVariant || 'cloud';
220
- const rendered = renderSkill(resolvedName, resolved);
221
- if (!rendered) {
222
- return textResult(`Skill '${resolvedName}' exists in the catalog but has no SKILL.md body on disk.`);
223
- }
224
- if (format === 'json') {
225
- return textResult(JSON.stringify({
226
- skill: getSkill(resolvedName),
227
- requested: name,
228
- resolved_name: resolvedName,
229
- variant: resolved,
230
- header: rendered.header,
231
- body: rendered.body,
232
- }, null, 2));
233
- }
234
- return textResult(`${rendered.header}\n\n${rendered.body}`);
235
- },
236
- );
237
-
238
- // ── rdc_skill_search ───────────────────────────────────────────────────────
239
- srv.registerTool(
240
- 'rdc_skill_search',
241
- {
242
- description:
243
- 'Fuzzy/substring search the rdc-skills catalog over name, slash, summary, and trigger phrases. Returns ranked matches.',
244
- inputSchema: {
245
- query: z.string().describe('Search terms, e.g. "deploy coolify" or "work items".'),
246
- },
247
- },
248
- async ({ query }) => {
249
- const results = searchSkills(query);
250
- return textResult(JSON.stringify({ query, count: results.length, results }, null, 2));
251
- },
252
- );
253
-
254
- return srv;
255
- }
256
-
257
- function startHttp() {
258
- const app = express();
259
- app.use(express.json({ limit: '4mb' }));
260
-
261
- // /health — open, cheap, reports live skill count.
262
- app.get('/health', (_req, res) => {
263
- let skills = 0;
264
- try {
265
- skills = listSkills().length;
266
- } catch {
267
- skills = 0;
268
- }
269
- res.json({ status: 'ok', service: 'rdc-skills-mcp', version: pkgVersion(), git_sha: GIT_SHA, skills });
270
- });
271
-
272
- // Block OAuth discovery so connectors skip OAuth and connect direct — /mcp is open.
273
- app.get('/.well-known/oauth-authorization-server', (_req, res) => res.status(404).end());
274
- app.get('/.well-known/openid-configuration', (_req, res) => res.status(404).end());
275
- app.get('/authorize', (_req, res) => res.status(404).end());
276
- app.post('/token', (_req, res) => res.status(404).end());
277
-
278
- // POST /mcp — stateless StreamableHTTP transport, no Authorization required.
279
- app.post('/mcp', async (req, res) => {
280
- try {
281
- if (!acceptsStreamableHttp(req)) {
282
- res.status(406).json(directMcpUsage('MCP Streamable HTTP requires Accept: application/json, text/event-stream'));
283
- return;
284
- }
285
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
286
- const srv = buildMcpServer();
287
- await srv.connect(transport);
288
- await transport.handleRequest(req, res, req.body);
289
- res.on('close', async () => {
290
- await transport.close().catch(() => {});
291
- await srv.close().catch(() => {});
292
- });
293
- } catch (err) {
294
- console.error('[rdc-skills-mcp] MCP error:', err);
295
- if (!res.headersSent) {
296
- res.status(500).json({
297
- jsonrpc: '2.0',
298
- error: { code: -32603, message: err?.message || 'internal error' },
299
- id: null,
300
- });
301
- }
302
- }
303
- });
304
-
305
- app.get('/mcp', (_req, res) => {
306
- res.status(405).json(directMcpUsage('Use POST for MCP requests.'));
307
- });
308
-
309
- app.listen(PORT, () => {
310
- console.log(`[rdc-skills-mcp] ready on port ${PORT}`);
311
- console.log(` MCP: POST http://localhost:${PORT}/mcp`);
312
- console.log(` Health: GET http://localhost:${PORT}/health`);
313
- });
314
- }
315
-
316
- startHttp();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/rdc-skills-mcp.mjs — local MCP server exposing the rdc-skills library.
4
+ *
5
+ * Mirrors the codeflow MCP topology (packages/codeflow/src/mcp/server.ts):
6
+ * express + @modelcontextprotocol/sdk McpServer + StreamableHTTPServerTransport,
7
+ * stateless (a fresh server+transport per POST /mcp), PORT from env.
8
+ *
9
+ * Routes:
10
+ * POST /mcp — MCP over StreamableHTTP. NO Authorization (URL is the shared
11
+ * secret, consistent with codeflow/clauth tunnel MCPs).
12
+ * GET /health — { status, service, version, skills } — no auth, no heavy work.
13
+ * /.well-known/* + /authorize + /token → 404 (force connectors to skip OAuth).
14
+ *
15
+ * It is tunneled in production at https://rdc-skills.regendevcorp.com/mcp; this
16
+ * process only listens on PORT (default 3110) and does not configure the tunnel.
17
+ *
18
+ * ── Caller detection → variant (best-effort) ────────────────────────────────
19
+ * Tools render a `cli` or `cloud` body. Detection rule: on MCP `initialize`,
20
+ * `clientInfo.name` containing `claude-code` or `codex` → `cli`; anything else
21
+ * (claude.ai web) → `cloud`; unknown → `cloud` (safer for web).
22
+ *
23
+ * APPROACH TAKEN — default + explicit override (NOT session-threaded):
24
+ * The transport is stateless (a new McpServer per POST, sessionIdGenerator:
25
+ * undefined), so there is no durable session to thread clientInfo through to a
26
+ * later tools/call. Rather than build a fragile session map, we:
27
+ * - capture clientInfo on `initialize` and remember the MOST RECENT one
28
+ * process-wide as a soft default (helps the common single-client case), and
29
+ * - ALWAYS honor an explicit `variant` arg on rdc_skill_get (the must-have).
30
+ * If no explicit variant is given and no client has initialized this process,
31
+ * the default is `cloud`. This keeps the override correct and the auto-detect
32
+ * best-effort, exactly as the spec permits.
33
+ */
34
+
35
+ import express from 'express';
36
+ import { z } from 'zod';
37
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
38
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
39
+ import fs from 'node:fs';
40
+ import path from 'node:path';
41
+ import { fileURLToPath } from 'node:url';
42
+ import { execFileSync } from 'node:child_process';
43
+
44
+ import {
45
+ listSkills,
46
+ getSkill,
47
+ getSkillBody,
48
+ getCloudOverride,
49
+ resolveSkillName,
50
+ searchSkills,
51
+ } from '../lib/catalog.mjs';
52
+ import { toCloudBody } from '../lib/cloud-rewrite.mjs';
53
+
54
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
55
+ const REPO_ROOT = path.resolve(__dirname, '..');
56
+ const PORT = parseInt(process.env.PORT || '3110', 10);
57
+
58
+ function pkgVersion() {
59
+ try {
60
+ return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version || '0.0.0';
61
+ } catch {
62
+ return '0.0.0';
63
+ }
64
+ }
65
+
66
+ // Commit the running process actually LOADED — resolved ONCE at startup, never
67
+ // per-request, so /health reports a PROVABLE commit. Resolution order:
68
+ // 1. git-sha.json — baked at pack/publish (scripts/stamp-git-sha.mjs). This is
69
+ // the PRODUCTION path: the process runs from the npm install (no .git), so
70
+ // runtime rev-parse can't work — the stamped file is the source of truth.
71
+ // 2. runtime `git rev-parse` — the DEV path (running straight from the checkout).
72
+ // 3. env override, then 'unknown'.
73
+ const GIT_SHA = (() => {
74
+ try {
75
+ const stamped = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'git-sha.json'), 'utf8')).sha;
76
+ if (stamped && stamped !== 'unknown') return stamped;
77
+ } catch { /* not stamped — fall through to runtime/dev resolution */ }
78
+ try {
79
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
80
+ cwd: REPO_ROOT,
81
+ encoding: 'utf8',
82
+ stdio: ['ignore', 'pipe', 'ignore'],
83
+ }).trim();
84
+ } catch {
85
+ return process.env.RDC_SKILLS_GIT_SHA || 'unknown';
86
+ }
87
+ })();
88
+
89
+ // Soft process-wide default variant, updated whenever a client initializes.
90
+ let lastDetectedVariant = 'cloud';
91
+
92
+ /** Map a clientInfo.name → variant. */
93
+ function variantForClient(clientName) {
94
+ const n = String(clientName || '').toLowerCase();
95
+ if (n.includes('claude-code') || n.includes('codex')) return 'cli';
96
+ return 'cloud'; // claude.ai web client and anything unknown
97
+ }
98
+
99
+ /**
100
+ * Render a skill body for the resolved variant.
101
+ * - cli → SKILL.md body unchanged.
102
+ * - cloud → SKILL.cloud.md verbatim if present, else toCloudBody(SKILL.md).
103
+ * Returns { header, body } or null if the skill has no body on disk.
104
+ */
105
+ function renderSkill(name, variant) {
106
+ const body = getSkillBody(name);
107
+ if (body == null) return null;
108
+ if (variant === 'cli') {
109
+ return { header: `<!-- rdc-skills: '${name}' served as CLI variant -->`, body };
110
+ }
111
+ const override = getCloudOverride(name);
112
+ if (override != null) {
113
+ return {
114
+ header: `<!-- rdc-skills: '${name}' served as CLOUD variant (hand-tuned SKILL.cloud.md) -->`,
115
+ body: override,
116
+ };
117
+ }
118
+ return {
119
+ header: `<!-- rdc-skills: '${name}' served as CLOUD variant (auto-rewritten from SKILL.md) -->`,
120
+ body: toCloudBody(body),
121
+ };
122
+ }
123
+
124
+ function textResult(text) {
125
+ return { content: [{ type: 'text', text }] };
126
+ }
127
+
128
+ function directMcpUsage(message) {
129
+ return {
130
+ jsonrpc: '2.0',
131
+ error: {
132
+ code: -32000,
133
+ message,
134
+ help: {
135
+ endpoint: '/mcp',
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ Accept: 'application/json, text/event-stream',
140
+ },
141
+ tools: ['rdc_skill_list', 'rdc_skill_search', 'rdc_skill_get'],
142
+ response: 'Streamable HTTP returns Server-Sent Events. Parse the JSON-RPC envelope from each data: line; tool text is at result.content[0].text.',
143
+ curl: `curl -s -X POST https://rdc-skills.regendevcorp.com/mcp \\
144
+ -H 'Content-Type: application/json' \\
145
+ -H 'Accept: application/json, text/event-stream' \\
146
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"rdc_skill_list","arguments":{}}}'`,
147
+ },
148
+ },
149
+ id: null,
150
+ };
151
+ }
152
+
153
+ function acceptsStreamableHttp(req) {
154
+ const accept = String(req.headers.accept || '');
155
+ return accept.includes('application/json') && accept.includes('text/event-stream');
156
+ }
157
+
158
+ function buildMcpServer() {
159
+ const srv = new McpServer({ name: 'rdc-skills', version: pkgVersion() });
160
+
161
+ // Capture clientInfo on initialize to refine the soft default variant.
162
+ // The SDK exposes the underlying low-level server; oninitialized fires after
163
+ // the client's initialize params are recorded.
164
+ try {
165
+ srv.server.oninitialized = () => {
166
+ const info = srv.server.getClientVersion?.();
167
+ if (info?.name) lastDetectedVariant = variantForClient(info.name);
168
+ };
169
+ } catch {
170
+ /* non-fatal: detection stays at the cloud default */
171
+ }
172
+
173
+ // ── rdc_skill_list ─────────────────────────────────────────────────────────
174
+ srv.registerTool(
175
+ 'rdc_skill_list',
176
+ {
177
+ description:
178
+ 'List the rdc-skills catalog: every skill with its slash form, category, summary, when-to-use triggers, usage, args, and required capabilities. No input required.',
179
+ inputSchema: {},
180
+ },
181
+ async () => {
182
+ const catalog = listSkills();
183
+ return textResult(JSON.stringify({ count: catalog.length, skills: catalog }, null, 2));
184
+ },
185
+ );
186
+
187
+ // ── rdc_skill_get ──────────────────────────────────────────────────────────
188
+ srv.registerTool(
189
+ 'rdc_skill_get',
190
+ {
191
+ description:
192
+ "Get a skill's SKILL.md body rendered for the caller. variant 'cli' returns the body unchanged; 'cloud' rewrites local-shell/clauth-daemon steps for the claude.ai web client. Omit variant to use auto-detection (defaults to cloud). format 'json' returns metadata plus rendered body for direct API/curl callers.",
193
+ inputSchema: {
194
+ name: z.string().describe('Skill name or slash form (e.g. "deploy", "rdc:build", "rdc:brochurify", "lifeai-brochure-author"). See rdc_skill_list.'),
195
+ variant: z.enum(['cli', 'cloud']).optional().describe('Force the rendered variant; overrides caller detection.'),
196
+ format: z.enum(['text', 'json']).optional().describe("Return 'text' (default) for agent-readable SKILL.md, or 'json' for metadata plus rendered body."),
197
+ },
198
+ },
199
+ async ({ name, variant, format }) => {
200
+ const resolvedName = resolveSkillName(name);
201
+ if (!resolvedName) {
202
+ const valid = listSkills().map((s) => ({
203
+ name: s.name,
204
+ slash: s.slash,
205
+ aliases: s.aliases,
206
+ usage: s.usage,
207
+ }));
208
+ if (format === 'json') {
209
+ return textResult(JSON.stringify({
210
+ error: 'unknown_skill',
211
+ requested: name,
212
+ message: `Unknown skill '${name}'.`,
213
+ valid_count: valid.length,
214
+ valid,
215
+ }, null, 2));
216
+ }
217
+ return textResult(`Unknown skill '${name}'. Valid skills (${valid.length}): ${valid.map((s) => `${s.name} (${s.slash})`).join(', ')}`);
218
+ }
219
+ const resolved = variant || lastDetectedVariant || 'cloud';
220
+ const rendered = renderSkill(resolvedName, resolved);
221
+ if (!rendered) {
222
+ return textResult(`Skill '${resolvedName}' exists in the catalog but has no SKILL.md body on disk.`);
223
+ }
224
+ if (format === 'json') {
225
+ return textResult(JSON.stringify({
226
+ skill: getSkill(resolvedName),
227
+ requested: name,
228
+ resolved_name: resolvedName,
229
+ variant: resolved,
230
+ header: rendered.header,
231
+ body: rendered.body,
232
+ }, null, 2));
233
+ }
234
+ return textResult(`${rendered.header}\n\n${rendered.body}`);
235
+ },
236
+ );
237
+
238
+ // ── rdc_skill_search ───────────────────────────────────────────────────────
239
+ srv.registerTool(
240
+ 'rdc_skill_search',
241
+ {
242
+ description:
243
+ 'Fuzzy/substring search the rdc-skills catalog over name, slash, summary, and trigger phrases. Returns ranked matches.',
244
+ inputSchema: {
245
+ query: z.string().describe('Search terms, e.g. "deploy coolify" or "work items".'),
246
+ },
247
+ },
248
+ async ({ query }) => {
249
+ const results = searchSkills(query);
250
+ return textResult(JSON.stringify({ query, count: results.length, results }, null, 2));
251
+ },
252
+ );
253
+
254
+ return srv;
255
+ }
256
+
257
+ function startHttp() {
258
+ const app = express();
259
+ app.use(express.json({ limit: '4mb' }));
260
+
261
+ // /health — open, cheap, reports live skill count.
262
+ app.get('/health', (_req, res) => {
263
+ let skills = 0;
264
+ try {
265
+ skills = listSkills().length;
266
+ } catch {
267
+ skills = 0;
268
+ }
269
+ res.json({ status: 'ok', service: 'rdc-skills-mcp', version: pkgVersion(), git_sha: GIT_SHA, skills });
270
+ });
271
+
272
+ // Block OAuth discovery so connectors skip OAuth and connect direct — /mcp is open.
273
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => res.status(404).end());
274
+ app.get('/.well-known/openid-configuration', (_req, res) => res.status(404).end());
275
+ app.get('/authorize', (_req, res) => res.status(404).end());
276
+ app.post('/token', (_req, res) => res.status(404).end());
277
+
278
+ // POST /mcp — stateless StreamableHTTP transport, no Authorization required.
279
+ app.post('/mcp', async (req, res) => {
280
+ try {
281
+ if (!acceptsStreamableHttp(req)) {
282
+ res.status(406).json(directMcpUsage('MCP Streamable HTTP requires Accept: application/json, text/event-stream'));
283
+ return;
284
+ }
285
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
286
+ const srv = buildMcpServer();
287
+ await srv.connect(transport);
288
+ await transport.handleRequest(req, res, req.body);
289
+ res.on('close', async () => {
290
+ await transport.close().catch(() => {});
291
+ await srv.close().catch(() => {});
292
+ });
293
+ } catch (err) {
294
+ console.error('[rdc-skills-mcp] MCP error:', err);
295
+ if (!res.headersSent) {
296
+ res.status(500).json({
297
+ jsonrpc: '2.0',
298
+ error: { code: -32603, message: err?.message || 'internal error' },
299
+ id: null,
300
+ });
301
+ }
302
+ }
303
+ });
304
+
305
+ app.get('/mcp', (_req, res) => {
306
+ res.status(405).json(directMcpUsage('Use POST for MCP requests.'));
307
+ });
308
+
309
+ app.listen(PORT, () => {
310
+ console.log(`[rdc-skills-mcp] ready on port ${PORT}`);
311
+ console.log(` MCP: POST http://localhost:${PORT}/mcp`);
312
+ console.log(` Health: GET http://localhost:${PORT}/health`);
313
+ });
314
+ }
315
+
316
+ startHttp();