@lifeaitools/rdc-skills 0.24.38 → 0.24.41

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 (196) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1371 -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 +376 -375
  10. package/README.sandbox.md +3 -3
  11. package/RELEASE.md +42 -0
  12. package/assets/watcher/viewer.html +164 -164
  13. package/bin/rdc-skills-mcp.mjs +316 -316
  14. package/commands/build.md +183 -183
  15. package/commands/collab.md +180 -180
  16. package/commands/deploy.md +152 -152
  17. package/commands/design.md +31 -31
  18. package/commands/edit.md +28 -28
  19. package/commands/fixit.md +124 -124
  20. package/commands/handoff.md +173 -173
  21. package/commands/help.md +95 -95
  22. package/commands/overnight.md +220 -220
  23. package/commands/plan.md +158 -158
  24. package/commands/preplan.md +131 -131
  25. package/commands/prototype.md +145 -145
  26. package/commands/release.md +49 -49
  27. package/commands/report.md +99 -99
  28. package/commands/review.md +120 -120
  29. package/commands/self-test.md +113 -113
  30. package/commands/status.md +86 -86
  31. package/commands/watch.md +98 -98
  32. package/commands/workitems.md +137 -137
  33. package/git-sha.json +1 -1
  34. package/guides/agent-bootstrap.md +295 -295
  35. package/guides/agents/backend.md +104 -104
  36. package/guides/agents/content.md +94 -94
  37. package/guides/agents/cs2.md +56 -56
  38. package/guides/agents/data.md +87 -87
  39. package/guides/agents/design.md +77 -77
  40. package/guides/agents/frontend.md +92 -92
  41. package/guides/agents/infrastructure.md +81 -81
  42. package/guides/agents/setup.md +281 -281
  43. package/guides/agents/verify.md +151 -151
  44. package/guides/agents/viz.md +106 -106
  45. package/guides/backend.md +146 -146
  46. package/guides/content.md +147 -147
  47. package/guides/cs2.md +190 -190
  48. package/guides/data.md +123 -123
  49. package/guides/design.md +116 -116
  50. package/guides/engineering-behavior.md +43 -43
  51. package/guides/escalation-protocol.md +125 -125
  52. package/guides/frontend.md +151 -151
  53. package/guides/history-md-spec.md +297 -297
  54. package/guides/infrastructure.md +179 -179
  55. package/guides/lessons-learned-spec.md +151 -145
  56. package/guides/output-contract.md +108 -108
  57. package/guides/publish-md-spec.md +289 -289
  58. package/guides/rdc-skills-startup.md +30 -30
  59. package/guides/verify.md +11 -11
  60. package/hooks/check-cwd.js +31 -31
  61. package/hooks/check-rdc-environment.js +164 -164
  62. package/hooks/check-services.js +6 -6
  63. package/hooks/check-stale-work-items.js +19 -19
  64. package/hooks/foreground-process-gate.js +128 -128
  65. package/hooks/gate-watchdog-selfcheck.js +257 -257
  66. package/hooks/hook-logger.js +25 -25
  67. package/hooks/lib/run-evidence-gate.mjs +241 -241
  68. package/hooks/no-stop-open-epics.js +127 -127
  69. package/hooks/post-tool-batch-gate.js +203 -203
  70. package/hooks/post-work-check.js +21 -21
  71. package/hooks/postcompact-log.js +13 -13
  72. package/hooks/precompact-log.js +13 -13
  73. package/hooks/rate-limit-retry.js +46 -46
  74. package/hooks/rdc-invocation-marker.js +157 -157
  75. package/hooks/rdc-output-contract-gate.js +94 -94
  76. package/hooks/require-work-item-on-commit.js +294 -294
  77. package/hooks/restart-brief.js +19 -19
  78. package/hooks/run-hidden-hook.ps1 +47 -47
  79. package/hooks/task-completed-gate.js +274 -274
  80. package/hooks/work-item-exit-gate.js +944 -944
  81. package/lib/catalog.mjs +236 -236
  82. package/lib/cloud-rewrite.mjs +155 -155
  83. package/package.json +57 -56
  84. package/rules/work-items-rpc.md +520 -520
  85. package/scaffold/templates/HISTORY.md.template +39 -39
  86. package/scaffold/templates/PUBLISH.md.template +21 -21
  87. package/scaffold/templates/brochure-studio-default.html +70 -70
  88. package/scripts/acceptance.mjs +502 -502
  89. package/scripts/fixtures/guides/bad-guide.md +15 -15
  90. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  91. package/scripts/install-rdc-skills.js +1289 -1289
  92. package/scripts/install.ps1 +202 -202
  93. package/scripts/install.sh +132 -132
  94. package/scripts/lib/assertions.mjs +287 -287
  95. package/scripts/lib/manifest-schema.mjs +754 -754
  96. package/scripts/lib/runner.mjs +465 -465
  97. package/scripts/lib/sandbox.mjs +435 -435
  98. package/scripts/prepack.mjs +32 -32
  99. package/scripts/rdc-brochure.mjs +482 -464
  100. package/scripts/rdc-design-cli.mjs +134 -134
  101. package/scripts/rebuild-mcp.mjs +107 -107
  102. package/scripts/self-test.mjs +1460 -1460
  103. package/scripts/stamp-git-sha.mjs +29 -29
  104. package/scripts/test-guide-validator.mjs +196 -196
  105. package/scripts/test-rdc-hooks.mjs +145 -145
  106. package/scripts/uninstall.ps1 +77 -77
  107. package/scripts/uninstall.sh +69 -69
  108. package/scripts/update.ps1 +43 -43
  109. package/scripts/update.sh +43 -43
  110. package/scripts/validate-place-histories.js +461 -461
  111. package/scripts/validate-publish-manifests.js +424 -424
  112. package/scripts/watch-init.mjs +100 -100
  113. package/skills/brochure/SKILL.md +107 -107
  114. package/skills/build/SKILL.md +563 -563
  115. package/skills/channel-formatter/SKILL.md +533 -533
  116. package/skills/co-develop/SKILL.md +196 -196
  117. package/skills/collab/SKILL.md +239 -239
  118. package/skills/convert/SKILL.md +140 -140
  119. package/skills/deploy/SKILL.md +541 -541
  120. package/skills/design/SKILL.md +211 -211
  121. package/skills/design/reference/ownership.md +16 -16
  122. package/skills/design/reference/rampa.md +92 -92
  123. package/skills/design/reference/studio-model.md +153 -153
  124. package/skills/edit/SKILL.md +98 -98
  125. package/skills/fixit/SKILL.md +165 -165
  126. package/skills/fs-mcp/SKILL.md +148 -148
  127. package/skills/handoff/SKILL.md +236 -200
  128. package/skills/help/SKILL.md +143 -143
  129. package/skills/housekeeping/SKILL.md +219 -160
  130. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  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/rdc-brochure.test.json +34 -34
  148. package/skills/tests/rdc-build.test.json +36 -36
  149. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  150. package/skills/tests/rdc-co-develop.test.json +29 -29
  151. package/skills/tests/rdc-collab.test.json +29 -29
  152. package/skills/tests/rdc-convert.test.json +35 -35
  153. package/skills/tests/rdc-deploy.test.json +30 -30
  154. package/skills/tests/rdc-design.test.json +27 -27
  155. package/skills/tests/rdc-edit.test.json +29 -29
  156. package/skills/tests/rdc-fixit.test.json +36 -36
  157. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  158. package/skills/tests/rdc-handoff.test.json +28 -28
  159. package/skills/tests/rdc-help.test.json +29 -29
  160. package/skills/tests/rdc-housekeeping.test.json +32 -28
  161. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  162. package/skills/tests/rdc-overnight.test.json +37 -37
  163. package/skills/tests/rdc-plan.test.json +27 -27
  164. package/skills/tests/rdc-preplan.test.json +31 -31
  165. package/skills/tests/rdc-prototype.test.json +28 -28
  166. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  167. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  168. package/skills/tests/rdc-release.test.json +29 -29
  169. package/skills/tests/rdc-report.test.json +28 -28
  170. package/skills/tests/rdc-review.test.json +29 -29
  171. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  172. package/skills/tests/rdc-self-test.test.json +24 -24
  173. package/skills/tests/rdc-status.test.json +29 -29
  174. package/skills/tests/rdc-terminal-config.test.json +29 -29
  175. package/skills/tests/rdc-watch.test.json +24 -24
  176. package/skills/tests/rdc-workitems.test.json +27 -27
  177. package/skills/watch/SKILL.md +97 -97
  178. package/skills/workitems/SKILL.md +151 -151
  179. package/tests/acceptance.test.mjs +59 -59
  180. package/tests/channel-formatter.contract.test.mjs +251 -251
  181. package/tests/curl-surface.test.mjs +289 -289
  182. package/tests/harness-gates.test.mjs +325 -325
  183. package/tests/help-surface.test.mjs +61 -61
  184. package/tests/housekeeping-lessons-triage.test.mjs +49 -0
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/lessons-pipeline-contract.test.mjs +26 -0
  187. package/tests/manifest-contract-fields.test.mjs +78 -78
  188. package/tests/mcp.test.mjs +271 -271
  189. package/tests/rdc-brochure.test.mjs +125 -0
  190. package/tests/release-contract.test.mjs +16 -0
  191. package/tests/require-work-item-on-commit.test.mjs +162 -162
  192. package/tests/run-evidence-gate.test.mjs +82 -82
  193. package/tests/skill-test-matrix.test.mjs +66 -66
  194. package/tests/validate-skills.js +27 -27
  195. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  196. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
@@ -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();