great-cto 2.3.4 → 2.5.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.
- package/dist/adapt.js +342 -0
- package/dist/ci.js +258 -0
- package/dist/main.js +135 -0
- package/dist/mcp.js +355 -0
- package/dist/report.js +410 -0
- package/dist/serve.js +289 -0
- package/dist/webhook-cli.js +150 -0
- package/dist/webhook-config.js +65 -0
- package/dist/webhook-dispatch.js +132 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -83,6 +83,18 @@ function parseArgs(argv) {
|
|
|
83
83
|
args.command = "scan";
|
|
84
84
|
else if (a === "list-rules")
|
|
85
85
|
args.command = "list-rules";
|
|
86
|
+
else if (a === "ci")
|
|
87
|
+
args.command = "ci";
|
|
88
|
+
else if (a === "mcp")
|
|
89
|
+
args.command = "mcp";
|
|
90
|
+
else if (a === "adapt")
|
|
91
|
+
args.command = "adapt";
|
|
92
|
+
else if (a === "serve")
|
|
93
|
+
args.command = "serve";
|
|
94
|
+
else if (a === "webhook")
|
|
95
|
+
args.command = "webhook";
|
|
96
|
+
else if (a === "report")
|
|
97
|
+
args.command = "report";
|
|
86
98
|
else if (a.startsWith("--dir="))
|
|
87
99
|
args.dir = a.slice("--dir=".length);
|
|
88
100
|
else if (a === "--dir")
|
|
@@ -316,6 +328,10 @@ ${bold("Usage:")}
|
|
|
316
328
|
npx great-cto register [--dir PATH]
|
|
317
329
|
npx great-cto scan [path] [--severity LVL] [--scanner NAME] [--sarif FILE]
|
|
318
330
|
npx great-cto list-rules
|
|
331
|
+
npx great-cto ci [path] [--fail-on LVL] [--sarif F] [--junit F]
|
|
332
|
+
npx great-cto mcp [--sse --port N]
|
|
333
|
+
npx great-cto adapt --platform [claude|codex|cursor|aider|continue|all]
|
|
334
|
+
npx great-cto serve [--port 3142]
|
|
319
335
|
npx great-cto help
|
|
320
336
|
npx great-cto version
|
|
321
337
|
|
|
@@ -338,6 +354,32 @@ ${bold("Scan (AI-security):")}
|
|
|
338
354
|
great-cto list-rules Print rule catalog
|
|
339
355
|
${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
|
|
340
356
|
|
|
357
|
+
${bold("CI gate:")}
|
|
358
|
+
great-cto ci Single-command CI gate (scan + archetype check)
|
|
359
|
+
great-cto ci --fail-on critical Exit 1 only on critical findings (default)
|
|
360
|
+
great-cto ci --sarif out.sarif Emit SARIF (uploadable to GitHub Security)
|
|
361
|
+
great-cto ci --junit out.xml Emit JUnit XML for test reporters
|
|
362
|
+
${dim("(auto-detects \$GITHUB_ACTIONS → emits ::error:: annotations)")}
|
|
363
|
+
|
|
364
|
+
${bold("MCP server (cross-platform):")}
|
|
365
|
+
great-cto mcp Stdio MCP server — works in Claude Desktop /
|
|
366
|
+
Cursor / Continue / any MCP host
|
|
367
|
+
great-cto mcp --sse --port 8765 SSE mode for remote / multi-client (TODO v2.5)
|
|
368
|
+
${dim("Tools exposed: scan, list_rules, detect_archetype, estimate_cost, query_decisions")}
|
|
369
|
+
|
|
370
|
+
${bold("Platform adapter (multi-tool support):")}
|
|
371
|
+
great-cto adapt --platform claude Generate AGENTS.md + CLAUDE.md
|
|
372
|
+
great-cto adapt --platform codex Generate AGENTS.md (OpenAI Codex CLI)
|
|
373
|
+
great-cto adapt --platform cursor Generate .cursorrules + .cursor/rules/*.mdc
|
|
374
|
+
great-cto adapt --platform aider Generate .aider.conf.yml + CONVENTIONS.md
|
|
375
|
+
great-cto adapt --platform continue Generate .continue/rules.md
|
|
376
|
+
great-cto adapt --platform all All of the above
|
|
377
|
+
${dim("Idempotent — re-run after editing .great_cto/PROJECT.md")}
|
|
378
|
+
|
|
379
|
+
${bold("Webhook server (preview):")}
|
|
380
|
+
great-cto serve --port 3142 Webhook receiver (logs to ~/.great_cto/webhook-events.log)
|
|
381
|
+
${dim("Endpoints: POST /webhook/github, POST /webhook/generic, GET /events, GET /healthz")}
|
|
382
|
+
|
|
341
383
|
${bold("Options:")}
|
|
342
384
|
-y, --yes Skip confirmation prompts (non-interactive)
|
|
343
385
|
--dry-run Show what would be done without doing it
|
|
@@ -732,6 +774,99 @@ async function main() {
|
|
|
732
774
|
process.exit(1);
|
|
733
775
|
}
|
|
734
776
|
}
|
|
777
|
+
if (args.command === "ci") {
|
|
778
|
+
try {
|
|
779
|
+
const { runCi, parseCiArgs } = await import("./ci.js");
|
|
780
|
+
const code = await runCi(parseCiArgs(rawArgv));
|
|
781
|
+
process.exit(code);
|
|
782
|
+
}
|
|
783
|
+
catch (e) {
|
|
784
|
+
error(e.message);
|
|
785
|
+
process.exit(2);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (args.command === "mcp") {
|
|
789
|
+
try {
|
|
790
|
+
const { runMcp } = await import("./mcp.js");
|
|
791
|
+
const sse = rawArgv.includes("--sse");
|
|
792
|
+
const portArg = rawArgv.indexOf("--port");
|
|
793
|
+
const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
|
|
794
|
+
const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
|
|
795
|
+
process.exit(code);
|
|
796
|
+
}
|
|
797
|
+
catch (e) {
|
|
798
|
+
error(e.message);
|
|
799
|
+
process.exit(2);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (args.command === "adapt") {
|
|
803
|
+
try {
|
|
804
|
+
const { runAdapt } = await import("./adapt.js");
|
|
805
|
+
const platArg = rawArgv.indexOf("--platform");
|
|
806
|
+
const platform = (platArg >= 0 ? rawArgv[platArg + 1] : "all");
|
|
807
|
+
const valid = ["claude", "codex", "cursor", "aider", "continue", "all"];
|
|
808
|
+
if (!valid.includes(platform)) {
|
|
809
|
+
error(`adapt: --platform must be one of ${valid.join(", ")} (got: ${platform})`);
|
|
810
|
+
process.exit(2);
|
|
811
|
+
}
|
|
812
|
+
const code = await runAdapt({
|
|
813
|
+
platform,
|
|
814
|
+
dryRun: rawArgv.includes("--dry-run"),
|
|
815
|
+
cwd: args.dir,
|
|
816
|
+
});
|
|
817
|
+
process.exit(code);
|
|
818
|
+
}
|
|
819
|
+
catch (e) {
|
|
820
|
+
error(e.message);
|
|
821
|
+
process.exit(2);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (args.command === "serve") {
|
|
825
|
+
try {
|
|
826
|
+
const { runServe } = await import("./serve.js");
|
|
827
|
+
const code = await runServe({
|
|
828
|
+
port: args.boardPort === 3141 ? 3142 : args.boardPort,
|
|
829
|
+
noLog: rawArgv.includes("--no-log"),
|
|
830
|
+
insecure: rawArgv.includes("--insecure"),
|
|
831
|
+
});
|
|
832
|
+
process.exit(code);
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
error(e.message);
|
|
836
|
+
process.exit(2);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (args.command === "webhook") {
|
|
840
|
+
try {
|
|
841
|
+
const { runWebhookCli, parseWebhookArgs } = await import("./webhook-cli.js");
|
|
842
|
+
const parsed = parseWebhookArgs(rawArgv);
|
|
843
|
+
if (!parsed) {
|
|
844
|
+
error("usage: great-cto webhook list | add-incoming <name> --secret <s> | add-outgoing <name> --url <u> --format <f> --triggers <t1,t2> | remove <name> | test <name>");
|
|
845
|
+
process.exit(2);
|
|
846
|
+
}
|
|
847
|
+
const code = await runWebhookCli(parsed);
|
|
848
|
+
process.exit(code);
|
|
849
|
+
}
|
|
850
|
+
catch (e) {
|
|
851
|
+
error(e.message);
|
|
852
|
+
process.exit(2);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (args.command === "report") {
|
|
856
|
+
try {
|
|
857
|
+
const { runReport, parseReportArgs } = await import("./report.js");
|
|
858
|
+
const parsed = parseReportArgs(rawArgv, args.dir);
|
|
859
|
+
if (!parsed) {
|
|
860
|
+
process.exit(2);
|
|
861
|
+
}
|
|
862
|
+
const code = await runReport(parsed);
|
|
863
|
+
process.exit(code);
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
error(e.message);
|
|
867
|
+
process.exit(2);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
735
870
|
if (args.command === "version") {
|
|
736
871
|
// Version resolved in index.mjs or from package.json at runtime
|
|
737
872
|
try {
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// great-cto mcp — Model Context Protocol server.
|
|
2
|
+
//
|
|
3
|
+
// Exposes great_cto's core capabilities as MCP tools so any MCP-compatible
|
|
4
|
+
// host (Claude Desktop, Cursor, Continue, Codex CLI via MCP, custom agents)
|
|
5
|
+
// can call them. This is the single biggest cross-platform multiplier — one
|
|
6
|
+
// implementation, all clients.
|
|
7
|
+
//
|
|
8
|
+
// Modes:
|
|
9
|
+
// stdio (default) — Claude Desktop / Cursor / Continue spawn this as subprocess
|
|
10
|
+
// sse — long-running HTTP/SSE for remote / multi-client access
|
|
11
|
+
//
|
|
12
|
+
// Tools exposed:
|
|
13
|
+
// scan OWASP LLM Top 10 + 24 rules → findings
|
|
14
|
+
// list_rules full rule catalogue
|
|
15
|
+
// detect_archetype archetype + compliance for a path
|
|
16
|
+
// estimate_cost LLM/human time for a task
|
|
17
|
+
// query_decisions search ~/.great_cto/decisions.md
|
|
18
|
+
//
|
|
19
|
+
// Protocol: minimal MCP 2024-11-05 implementation. We hand-roll because
|
|
20
|
+
// adding @modelcontextprotocol/sdk would balloon install size for what is
|
|
21
|
+
// fundamentally a few JSON messages over stdio.
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join, resolve } from "node:path";
|
|
25
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
26
|
+
const SERVER_INFO = {
|
|
27
|
+
name: "great-cto",
|
|
28
|
+
version: "", // populated at startup
|
|
29
|
+
};
|
|
30
|
+
// ── Tool implementations ────────────────────────────────────────────────────
|
|
31
|
+
async function toolScan(args) {
|
|
32
|
+
const { scan } = await import("./agentshield/scanner.js");
|
|
33
|
+
const path = args.path ?? ".";
|
|
34
|
+
const report = scan(resolve(path), {
|
|
35
|
+
minSeverity: (args.severity ?? "info"),
|
|
36
|
+
scanners: args.scanner,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
files_scanned: report.filesScanned,
|
|
40
|
+
duration_ms: report.durationMs,
|
|
41
|
+
findings: report.findings.map((f) => ({
|
|
42
|
+
rule_id: f.rule.id,
|
|
43
|
+
severity: f.rule.severity,
|
|
44
|
+
title: f.rule.title,
|
|
45
|
+
owasp: f.rule.owasp,
|
|
46
|
+
file: f.location.file,
|
|
47
|
+
line: f.location.line,
|
|
48
|
+
snippet: f.location.snippet,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function toolListRules() {
|
|
53
|
+
const { loadRules } = await import("./agentshield/rules-loader.js");
|
|
54
|
+
const rules = loadRules();
|
|
55
|
+
return {
|
|
56
|
+
count: rules.length,
|
|
57
|
+
rules: rules.map((r) => ({
|
|
58
|
+
id: r.id,
|
|
59
|
+
severity: r.severity,
|
|
60
|
+
scanner: r.scanner,
|
|
61
|
+
title: r.title,
|
|
62
|
+
owasp: r.owasp ?? null,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function toolDetectArchetype(args) {
|
|
67
|
+
const { detect } = await import("./detect.js");
|
|
68
|
+
const { pickArchetype, suggestCompliance } = await import("./archetypes.js");
|
|
69
|
+
const cwd = resolve(args.path ?? ".");
|
|
70
|
+
const detected = await detect(cwd);
|
|
71
|
+
const result = pickArchetype(detected);
|
|
72
|
+
const compliance = suggestCompliance(detected, result.primary);
|
|
73
|
+
return {
|
|
74
|
+
archetype: result.primary,
|
|
75
|
+
confidence: result.confidence,
|
|
76
|
+
rationale: result.rationale,
|
|
77
|
+
alternatives: result.alternatives,
|
|
78
|
+
compliance,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function toolEstimateCost(args) {
|
|
82
|
+
// Rough heuristic — for production, agents call /cost feature directly.
|
|
83
|
+
// This is the LLM-host-friendly summary.
|
|
84
|
+
const scale = args.scale ?? "standard";
|
|
85
|
+
const minutesByScale = {
|
|
86
|
+
quick: 15,
|
|
87
|
+
standard: 45,
|
|
88
|
+
deep: 90,
|
|
89
|
+
};
|
|
90
|
+
const llmRatePerHr = 0.02;
|
|
91
|
+
const humanRatePerHr = 150;
|
|
92
|
+
const minutes = minutesByScale[scale] ?? 45;
|
|
93
|
+
const llmUsd = +(minutes / 60 * llmRatePerHr).toFixed(4);
|
|
94
|
+
const humanUsd = +(minutes / 60 * humanRatePerHr).toFixed(2);
|
|
95
|
+
const humanDays = +(minutes / 60 / 6).toFixed(1); // 6 productive hrs / day
|
|
96
|
+
return {
|
|
97
|
+
task: args.task_description ?? "",
|
|
98
|
+
archetype: args.archetype ?? "unknown",
|
|
99
|
+
scale,
|
|
100
|
+
llm_agent: { wall_clock_min: minutes, cost_usd: llmUsd },
|
|
101
|
+
human_team: { working_days: humanDays, cost_usd: humanUsd },
|
|
102
|
+
savings_x: Math.round(humanUsd / Math.max(llmUsd, 0.0001)),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function toolQueryDecisions(args) {
|
|
106
|
+
const decisionsPath = join(homedir(), ".great_cto", "decisions.md");
|
|
107
|
+
if (!existsSync(decisionsPath)) {
|
|
108
|
+
return { count: 0, results: [], note: "No ~/.great_cto/decisions.md found." };
|
|
109
|
+
}
|
|
110
|
+
const text = readFileSync(decisionsPath, "utf8");
|
|
111
|
+
const entries = text.split(/\n---\n/).filter(s => s.trim());
|
|
112
|
+
const q = (args.query ?? "").toLowerCase();
|
|
113
|
+
const limit = args.limit ?? 10;
|
|
114
|
+
const matches = q
|
|
115
|
+
? entries.filter(e => e.toLowerCase().includes(q))
|
|
116
|
+
: entries.slice(-limit);
|
|
117
|
+
return {
|
|
118
|
+
count: matches.length,
|
|
119
|
+
total_decisions: entries.length,
|
|
120
|
+
results: matches.slice(0, limit).map(e => {
|
|
121
|
+
const titleMatch = e.match(/^##\s+(.+)$/m);
|
|
122
|
+
return {
|
|
123
|
+
title: titleMatch?.[1] ?? "(untitled)",
|
|
124
|
+
excerpt: e.slice(0, 400),
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ── Tool dispatch table ────────────────────────────────────────────────────
|
|
130
|
+
const TOOLS = [
|
|
131
|
+
{
|
|
132
|
+
name: "scan",
|
|
133
|
+
description: "Scan code for AI/LLM-specific security issues (OWASP LLM Top 10, 24 rules). Returns findings with severity, file, line, OWASP mapping.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
path: { type: "string", description: "File or directory to scan (default: cwd)" },
|
|
138
|
+
severity: {
|
|
139
|
+
type: "string",
|
|
140
|
+
enum: ["info", "low", "medium", "high", "critical"],
|
|
141
|
+
description: "Minimum severity to report",
|
|
142
|
+
},
|
|
143
|
+
scanner: {
|
|
144
|
+
type: "array",
|
|
145
|
+
items: {
|
|
146
|
+
type: "string",
|
|
147
|
+
enum: ["prompt-injection", "secrets-in-prompts", "ssrf-in-tools", "rag-poisoning", "cost-runaway"],
|
|
148
|
+
},
|
|
149
|
+
description: "Limit to specific scanner categories",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
handler: toolScan,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "list_rules",
|
|
157
|
+
description: "List all 24 AgentShield security rules with severity and OWASP LLM Top 10 mapping.",
|
|
158
|
+
inputSchema: { type: "object", properties: {} },
|
|
159
|
+
handler: toolListRules,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "detect_archetype",
|
|
163
|
+
description: "Detect the project archetype (one of 25: fintech, healthcare, commerce, agent-product, mlops, edtech, gov-public, insurance, ...) and the compliance gates that apply.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
path: { type: "string", description: "Project root (default: cwd)" },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
handler: toolDetectArchetype,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "estimate_cost",
|
|
174
|
+
description: "Estimate LLM-agent wall-clock time and cost vs human-team equivalent for a task. Returns LLM/human cost and savings ratio (~7500×).",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
task_description: { type: "string" },
|
|
179
|
+
archetype: { type: "string" },
|
|
180
|
+
scale: { type: "string", enum: ["quick", "standard", "deep"] },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
handler: toolEstimateCost,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "query_decisions",
|
|
187
|
+
description: "Search the cross-project ADR log at ~/.great_cto/decisions.md. Returns matching decisions for the given query string.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
query: { type: "string", description: "Search string (case-insensitive). Empty = recent decisions." },
|
|
192
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
handler: toolQueryDecisions,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
// ── JSON-RPC handler ───────────────────────────────────────────────────────
|
|
199
|
+
async function handle(req) {
|
|
200
|
+
const { method, id = null, params } = req;
|
|
201
|
+
// Notifications (no id) get no response
|
|
202
|
+
const isNotification = id === null || id === undefined;
|
|
203
|
+
const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: id, result };
|
|
204
|
+
const fail = (code, message, data) => isNotification ? null : { jsonrpc: "2.0", id: id, error: { code, message, data } };
|
|
205
|
+
try {
|
|
206
|
+
switch (method) {
|
|
207
|
+
case "initialize":
|
|
208
|
+
return reply({
|
|
209
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
210
|
+
capabilities: { tools: {} },
|
|
211
|
+
serverInfo: SERVER_INFO,
|
|
212
|
+
});
|
|
213
|
+
case "initialized":
|
|
214
|
+
case "notifications/initialized":
|
|
215
|
+
return null;
|
|
216
|
+
case "tools/list":
|
|
217
|
+
return reply({
|
|
218
|
+
tools: TOOLS.map(t => ({
|
|
219
|
+
name: t.name,
|
|
220
|
+
description: t.description,
|
|
221
|
+
inputSchema: t.inputSchema,
|
|
222
|
+
})),
|
|
223
|
+
});
|
|
224
|
+
case "tools/call": {
|
|
225
|
+
const name = params?.name;
|
|
226
|
+
const args = params?.arguments ?? {};
|
|
227
|
+
const tool = TOOLS.find(t => t.name === name);
|
|
228
|
+
if (!tool)
|
|
229
|
+
return fail(-32601, `Unknown tool: ${name}`);
|
|
230
|
+
const result = await tool.handler(args);
|
|
231
|
+
return reply({
|
|
232
|
+
content: [
|
|
233
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
234
|
+
],
|
|
235
|
+
isError: false,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
case "ping":
|
|
239
|
+
return reply({});
|
|
240
|
+
default:
|
|
241
|
+
return fail(-32601, `Method not found: ${method}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return fail(-32603, `Internal error: ${e.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ── SSE transport ──────────────────────────────────────────────────────────
|
|
249
|
+
async function runSse(port, version) {
|
|
250
|
+
const { createServer } = await import("node:http");
|
|
251
|
+
// Each connection gets a unique session id and an open SSE stream.
|
|
252
|
+
// Inbound JSON-RPC arrives via POST /message?sessionId=<id>; responses are
|
|
253
|
+
// pushed back over the SSE stream. This matches the standard MCP SSE
|
|
254
|
+
// transport (https://spec.modelcontextprotocol.io/specification/transports).
|
|
255
|
+
const sessions = new Map();
|
|
256
|
+
const server = createServer(async (req, res) => {
|
|
257
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
258
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
259
|
+
const sessionId = `s-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
260
|
+
res.writeHead(200, {
|
|
261
|
+
"Content-Type": "text/event-stream",
|
|
262
|
+
"Cache-Control": "no-cache, no-transform",
|
|
263
|
+
Connection: "keep-alive",
|
|
264
|
+
"X-Accel-Buffering": "no",
|
|
265
|
+
});
|
|
266
|
+
// Initial endpoint event — tells client where to POST messages
|
|
267
|
+
res.write(`event: endpoint\ndata: /message?sessionId=${sessionId}\n\n`);
|
|
268
|
+
sessions.set(sessionId, res);
|
|
269
|
+
req.on("close", () => sessions.delete(sessionId));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (req.method === "POST" && url.pathname === "/message") {
|
|
273
|
+
const sessionId = url.searchParams.get("sessionId") ?? "";
|
|
274
|
+
const sse = sessions.get(sessionId);
|
|
275
|
+
if (!sse) {
|
|
276
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
277
|
+
res.end(JSON.stringify({ error: "unknown sessionId" }));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const chunks = [];
|
|
281
|
+
req.on("data", c => chunks.push(Buffer.from(c)));
|
|
282
|
+
req.on("end", async () => {
|
|
283
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
284
|
+
try {
|
|
285
|
+
const reqJson = JSON.parse(body);
|
|
286
|
+
const reply = await handle(reqJson);
|
|
287
|
+
if (reply) {
|
|
288
|
+
sse.write(`event: message\ndata: ${JSON.stringify(reply)}\n\n`);
|
|
289
|
+
}
|
|
290
|
+
res.writeHead(202).end();
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
294
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
300
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
301
|
+
res.end(JSON.stringify({ ok: true, version, sessions: sessions.size, transport: "sse" }));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
res.writeHead(404).end();
|
|
305
|
+
});
|
|
306
|
+
return new Promise(resolve => {
|
|
307
|
+
server.listen(port, "127.0.0.1", () => {
|
|
308
|
+
process.stderr.write(`great-cto mcp v${version} (sse) → http://localhost:${port}/sse\n`);
|
|
309
|
+
process.stderr.write(` GET /sse open event stream\n`);
|
|
310
|
+
process.stderr.write(` POST /message?sessionId=... send JSON-RPC\n`);
|
|
311
|
+
process.stderr.write(` GET /healthz liveness\n`);
|
|
312
|
+
});
|
|
313
|
+
process.on("SIGINT", () => { server.close(); resolve(0); });
|
|
314
|
+
process.on("SIGTERM", () => { server.close(); resolve(0); });
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// ── stdio transport ────────────────────────────────────────────────────────
|
|
318
|
+
async function runStdio() {
|
|
319
|
+
// Read newline-delimited JSON from stdin, write to stdout. This is the
|
|
320
|
+
// standard MCP stdio transport.
|
|
321
|
+
let buffer = "";
|
|
322
|
+
process.stdin.setEncoding("utf8");
|
|
323
|
+
process.stdin.on("data", async (chunk) => {
|
|
324
|
+
buffer += chunk;
|
|
325
|
+
let nl;
|
|
326
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
327
|
+
const line = buffer.slice(0, nl).trim();
|
|
328
|
+
buffer = buffer.slice(nl + 1);
|
|
329
|
+
if (!line)
|
|
330
|
+
continue;
|
|
331
|
+
try {
|
|
332
|
+
const req = JSON.parse(line);
|
|
333
|
+
const res = await handle(req);
|
|
334
|
+
if (res)
|
|
335
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
process.stderr.write(`mcp: parse error: ${e.message}\n`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return new Promise(resolve => {
|
|
343
|
+
process.stdin.on("end", () => resolve(0));
|
|
344
|
+
process.stdin.on("error", () => resolve(2));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
export async function runMcp(args) {
|
|
348
|
+
SERVER_INFO.version = args.version;
|
|
349
|
+
if (args.mode === "sse") {
|
|
350
|
+
return runSse(args.port, args.version);
|
|
351
|
+
}
|
|
352
|
+
// Notify clients we're ready (some hosts log this)
|
|
353
|
+
process.stderr.write(`great-cto mcp v${args.version} (stdio) — ${TOOLS.length} tools\n`);
|
|
354
|
+
return runStdio();
|
|
355
|
+
}
|