@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.
- package/.claude/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1371 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +376 -375
- package/README.sandbox.md +3 -3
- package/RELEASE.md +42 -0
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +124 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +151 -145
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +57 -56
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +482 -464
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +424 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -563
- package/skills/channel-formatter/SKILL.md +533 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +140 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +165 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -200
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +219 -160
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +32 -28
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/housekeeping-lessons-triage.test.mjs +49 -0
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/lessons-pipeline-contract.test.mjs +26 -0
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/rdc-brochure.test.mjs +125 -0
- package/tests/release-contract.test.mjs +16 -0
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
package/bin/rdc-skills-mcp.mjs
CHANGED
|
@@ -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();
|