@syke1/mcp-server 1.7.1 → 1.8.1
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/ai/project-scanner.d.ts +7 -0
- package/dist/ai/project-scanner.js +263 -0
- package/dist/index.js +67 -26
- package/dist/license/validator.d.ts +1 -1
- package/dist/license/validator.js +17 -5
- package/dist/web/server.js +6 -6
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Scanner — Cortex-only tool.
|
|
3
|
+
* Scans the entire project dependency graph and generates a comprehensive
|
|
4
|
+
* onboarding document using AI (BYOK).
|
|
5
|
+
*/
|
|
6
|
+
import { DependencyGraph } from "../graph";
|
|
7
|
+
export declare function scanProject(graph: DependencyGraph): Promise<string>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project Scanner — Cortex-only tool.
|
|
4
|
+
* Scans the entire project dependency graph and generates a comprehensive
|
|
5
|
+
* onboarding document using AI (BYOK).
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.scanProject = scanProject;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const provider_1 = require("./provider");
|
|
45
|
+
const context_extractor_1 = require("./context-extractor");
|
|
46
|
+
// ── Config file names to look for ──
|
|
47
|
+
const CONFIG_FILE_NAMES = [
|
|
48
|
+
"package.json", "tsconfig.json", "tsconfig.base.json",
|
|
49
|
+
"Cargo.toml", "go.mod", "go.sum",
|
|
50
|
+
"pubspec.yaml", "build.gradle", "pom.xml",
|
|
51
|
+
"Gemfile", "requirements.txt", "pyproject.toml", "setup.py",
|
|
52
|
+
"Makefile", "CMakeLists.txt",
|
|
53
|
+
"docker-compose.yml", "Dockerfile",
|
|
54
|
+
".env.example",
|
|
55
|
+
];
|
|
56
|
+
const MAX_CONFIG_LINES = 200;
|
|
57
|
+
// ── Core ──
|
|
58
|
+
async function scanProject(graph) {
|
|
59
|
+
const provider = (0, provider_1.getAIProvider)();
|
|
60
|
+
if (!provider) {
|
|
61
|
+
return "scan_project requires an AI API key.\n\nSet one of: GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY.";
|
|
62
|
+
}
|
|
63
|
+
const ctx = collectProjectContext(graph);
|
|
64
|
+
const systemPrompt = buildScanSystemPrompt();
|
|
65
|
+
const userPrompt = buildScanUserPrompt(ctx);
|
|
66
|
+
try {
|
|
67
|
+
return await provider.analyze(systemPrompt, userPrompt);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return `AI analysis error: ${err.message || err}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Context Collection ──
|
|
74
|
+
function collectProjectContext(graph) {
|
|
75
|
+
const projectRoot = graph.projectRoot;
|
|
76
|
+
// 1. Hub files — sorted by reverse dependency count
|
|
77
|
+
const hubFiles = getHubFiles(graph, 10);
|
|
78
|
+
// 2. Entry points — files with 0 reverse dependencies
|
|
79
|
+
const entryPoints = getEntryPoints(graph);
|
|
80
|
+
// 3. Circular dependencies (SCC clusters with size > 1)
|
|
81
|
+
const circularDependencies = getCircularDeps(graph);
|
|
82
|
+
// 4. Directory structure (depth-limited tree)
|
|
83
|
+
const directoryStructure = buildDirectoryTree(graph);
|
|
84
|
+
// 5. Config files
|
|
85
|
+
const configFiles = readConfigFiles(projectRoot);
|
|
86
|
+
// 6. Key file signatures (top 5 hub files)
|
|
87
|
+
const keyFileSignatures = buildKeySignatures(hubFiles.slice(0, 5), graph);
|
|
88
|
+
return {
|
|
89
|
+
projectRoot,
|
|
90
|
+
languages: graph.languages,
|
|
91
|
+
totalFiles: graph.files.size,
|
|
92
|
+
directoryStructure,
|
|
93
|
+
hubFiles,
|
|
94
|
+
entryPoints,
|
|
95
|
+
circularDependencies,
|
|
96
|
+
configFiles,
|
|
97
|
+
keyFileSignatures,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function getHubFiles(graph, topN) {
|
|
101
|
+
const counts = [];
|
|
102
|
+
for (const file of graph.files) {
|
|
103
|
+
const revDeps = graph.reverse.get(file) || [];
|
|
104
|
+
counts.push({ path: file, count: revDeps.length });
|
|
105
|
+
}
|
|
106
|
+
counts.sort((a, b) => b.count - a.count);
|
|
107
|
+
return counts.slice(0, topN).map((c) => ({
|
|
108
|
+
path: c.path,
|
|
109
|
+
relativePath: path.relative(graph.sourceDir, c.path).replace(/\\/g, "/"),
|
|
110
|
+
dependentCount: c.count,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
function getEntryPoints(graph) {
|
|
114
|
+
const entries = [];
|
|
115
|
+
for (const file of graph.files) {
|
|
116
|
+
const revDeps = graph.reverse.get(file) || [];
|
|
117
|
+
if (revDeps.length === 0) {
|
|
118
|
+
entries.push(path.relative(graph.sourceDir, file).replace(/\\/g, "/"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return entries.slice(0, 20); // cap at 20
|
|
122
|
+
}
|
|
123
|
+
function getCircularDeps(graph) {
|
|
124
|
+
if (!graph.scc)
|
|
125
|
+
return [];
|
|
126
|
+
return graph.scc.components
|
|
127
|
+
.filter((c) => c.length > 1)
|
|
128
|
+
.map((cluster) => cluster.map((f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/")));
|
|
129
|
+
}
|
|
130
|
+
function buildDirectoryTree(graph) {
|
|
131
|
+
// Build a set of relative directory paths from file list
|
|
132
|
+
const dirs = new Map(); // dir -> file count
|
|
133
|
+
for (const file of graph.files) {
|
|
134
|
+
const rel = path.relative(graph.sourceDir, file).replace(/\\/g, "/");
|
|
135
|
+
const parts = rel.split("/");
|
|
136
|
+
// Count files per top-level and second-level directory
|
|
137
|
+
for (let depth = 1; depth <= Math.min(parts.length - 1, 3); depth++) {
|
|
138
|
+
const dirPath = parts.slice(0, depth).join("/");
|
|
139
|
+
dirs.set(dirPath, (dirs.get(dirPath) || 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Sort and format as tree
|
|
143
|
+
const sorted = [...dirs.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
144
|
+
const lines = [];
|
|
145
|
+
for (const [dirPath, count] of sorted) {
|
|
146
|
+
const depth = dirPath.split("/").length - 1;
|
|
147
|
+
const indent = " ".repeat(depth);
|
|
148
|
+
const name = dirPath.split("/").pop();
|
|
149
|
+
lines.push(`${indent}${name}/ (${count} files)`);
|
|
150
|
+
}
|
|
151
|
+
return lines.join("\n") || "(flat structure)";
|
|
152
|
+
}
|
|
153
|
+
function readConfigFiles(projectRoot) {
|
|
154
|
+
const configs = [];
|
|
155
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
156
|
+
const filePath = path.join(projectRoot, name);
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(filePath)) {
|
|
159
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
160
|
+
const lines = content.split("\n");
|
|
161
|
+
const truncated = lines.length > MAX_CONFIG_LINES
|
|
162
|
+
? lines.slice(0, MAX_CONFIG_LINES).join("\n") + `\n... (${lines.length - MAX_CONFIG_LINES} more lines)`
|
|
163
|
+
: content;
|
|
164
|
+
configs.push({ path: filePath, name, content: truncated });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// skip unreadable files
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return configs;
|
|
172
|
+
}
|
|
173
|
+
function buildKeySignatures(hubFiles, graph) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
for (const hub of hubFiles) {
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(hub.path, "utf-8");
|
|
178
|
+
const lang = graph.languages[0] || "typescript";
|
|
179
|
+
const summary = (0, context_extractor_1.buildSmartContext)(content, lang, 80);
|
|
180
|
+
parts.push(`### ${hub.relativePath} (${hub.dependentCount} dependents)\n${summary}`);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
parts.push(`### ${hub.relativePath} (${hub.dependentCount} dependents)\n(unable to read)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return parts.join("\n\n");
|
|
187
|
+
}
|
|
188
|
+
// ── AI Prompts ──
|
|
189
|
+
function buildScanSystemPrompt() {
|
|
190
|
+
return `You are a senior software architect generating a comprehensive codebase onboarding document.
|
|
191
|
+
Based on the structural analysis data provided (dependency graph metrics, hub file rankings, circular dependencies, file signatures, and config files), generate a document following this exact structure:
|
|
192
|
+
|
|
193
|
+
## 1. Project Overview
|
|
194
|
+
Brief description of what this project does, its tech stack, and key technologies.
|
|
195
|
+
|
|
196
|
+
## 2. Architecture Overview
|
|
197
|
+
High-level architecture pattern (monolith, microservices, layered, etc.) and how components connect.
|
|
198
|
+
|
|
199
|
+
## 3. Repository Structure
|
|
200
|
+
Explain the directory layout and what each major directory contains.
|
|
201
|
+
|
|
202
|
+
## 4. Module / Component Map
|
|
203
|
+
Map out the key modules, their responsibilities, and how they relate to each other.
|
|
204
|
+
|
|
205
|
+
## 5. Key Concepts & Domain Model
|
|
206
|
+
Core domain concepts and abstractions that a new developer must understand.
|
|
207
|
+
|
|
208
|
+
## 6. Development Workflow
|
|
209
|
+
How to build, test, and run the project based on config files found.
|
|
210
|
+
|
|
211
|
+
## 7. Architectural Decisions
|
|
212
|
+
Notable patterns, frameworks, or design choices evident from the code structure.
|
|
213
|
+
|
|
214
|
+
## 8. Cross-Cutting Concerns
|
|
215
|
+
Shared utilities, common patterns, error handling, logging approaches found.
|
|
216
|
+
|
|
217
|
+
## 9. Danger Zones & Common Tasks
|
|
218
|
+
High-risk files (hub files with many dependents), circular dependencies to be aware of, and safe patterns for common modifications.
|
|
219
|
+
|
|
220
|
+
## 10. Quick Reference
|
|
221
|
+
Entry points, key file paths, and the most important files a new team member should read first.
|
|
222
|
+
|
|
223
|
+
Use the dependency graph data, hub file rankings, circular dependencies, entry points, and file signatures to make your analysis accurate and specific to THIS project.
|
|
224
|
+
Be concise but comprehensive. Output in Markdown. Do not include generic advice — only project-specific insights.`;
|
|
225
|
+
}
|
|
226
|
+
function buildScanUserPrompt(ctx) {
|
|
227
|
+
const sections = [];
|
|
228
|
+
sections.push(`# Project Scan Data`);
|
|
229
|
+
sections.push(`- **Root:** ${ctx.projectRoot}`);
|
|
230
|
+
sections.push(`- **Languages:** ${ctx.languages.join(", ")}`);
|
|
231
|
+
sections.push(`- **Total files in graph:** ${ctx.totalFiles}`);
|
|
232
|
+
sections.push(`\n## Directory Structure\n\`\`\`\n${ctx.directoryStructure}\n\`\`\``);
|
|
233
|
+
sections.push(`\n## Hub Files (Top ${ctx.hubFiles.length} by dependent count)`);
|
|
234
|
+
for (const h of ctx.hubFiles) {
|
|
235
|
+
sections.push(`- **${h.relativePath}** — ${h.dependentCount} dependents`);
|
|
236
|
+
}
|
|
237
|
+
sections.push(`\n## Entry Points (${ctx.entryPoints.length} files with 0 reverse deps)`);
|
|
238
|
+
for (const e of ctx.entryPoints) {
|
|
239
|
+
sections.push(`- ${e}`);
|
|
240
|
+
}
|
|
241
|
+
if (ctx.circularDependencies.length > 0) {
|
|
242
|
+
sections.push(`\n## Circular Dependencies (${ctx.circularDependencies.length} clusters)`);
|
|
243
|
+
for (let i = 0; i < ctx.circularDependencies.length; i++) {
|
|
244
|
+
sections.push(`### Cluster ${i + 1} (${ctx.circularDependencies[i].length} files)`);
|
|
245
|
+
for (const f of ctx.circularDependencies[i]) {
|
|
246
|
+
sections.push(`- ${f}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
sections.push(`\n## Circular Dependencies\nNone detected.`);
|
|
252
|
+
}
|
|
253
|
+
if (ctx.configFiles.length > 0) {
|
|
254
|
+
sections.push(`\n## Config Files`);
|
|
255
|
+
for (const cfg of ctx.configFiles) {
|
|
256
|
+
sections.push(`### ${cfg.name}\n\`\`\`\n${cfg.content}\n\`\`\``);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (ctx.keyFileSignatures) {
|
|
260
|
+
sections.push(`\n## Key File Signatures (Top Hub Files)\n${ctx.keyFileSignatures}`);
|
|
261
|
+
}
|
|
262
|
+
return sections.join("\n");
|
|
263
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -76,20 +76,34 @@ function resolveFilePath(fileArg, projectRoot, sourceDir) {
|
|
|
76
76
|
// License state — set at startup
|
|
77
77
|
let licenseStatus = { plan: "free", source: "default" };
|
|
78
78
|
// Free tier limits
|
|
79
|
-
const FREE_MAX_FILES =
|
|
79
|
+
const FREE_MAX_FILES = 200;
|
|
80
80
|
function isPro() {
|
|
81
81
|
if (!(0, validator_1._verifyStatus)(licenseStatus)) {
|
|
82
82
|
licenseStatus = { plan: "free", source: "default" };
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
|
-
return
|
|
85
|
+
return ["pro", "pro_trial", "cortex"].includes(licenseStatus.plan);
|
|
86
|
+
}
|
|
87
|
+
function isCortex() {
|
|
88
|
+
if (!(0, validator_1._verifyStatus)(licenseStatus)) {
|
|
89
|
+
licenseStatus = { plan: "free", source: "default" };
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return licenseStatus.plan === "cortex";
|
|
93
|
+
}
|
|
94
|
+
function getCortexToolError(toolName) {
|
|
95
|
+
if (licenseStatus.error)
|
|
96
|
+
return `${toolName}: ${licenseStatus.error}`;
|
|
97
|
+
if (isPro())
|
|
98
|
+
return `${toolName} requires SYKE Cortex. Upgrade at https://syke.cloud/dashboard/`;
|
|
99
|
+
return `${toolName} requires SYKE Cortex. Sign up at https://syke.cloud`;
|
|
86
100
|
}
|
|
87
101
|
function getMaxFiles() {
|
|
88
102
|
return isPro() ? undefined : FREE_MAX_FILES;
|
|
89
103
|
}
|
|
90
104
|
/**
|
|
91
105
|
* Check if a resolved file path is within the free tier limit.
|
|
92
|
-
* Free set = first
|
|
106
|
+
* Free set = first 200 files sorted alphabetically by relative path.
|
|
93
107
|
*/
|
|
94
108
|
function isFileInFreeSet(resolvedPath, graph) {
|
|
95
109
|
if (isPro())
|
|
@@ -98,7 +112,7 @@ function isFileInFreeSet(resolvedPath, graph) {
|
|
|
98
112
|
const idx = allFiles.indexOf(resolvedPath);
|
|
99
113
|
return idx >= 0 && idx < FREE_MAX_FILES;
|
|
100
114
|
}
|
|
101
|
-
const PRO_UPGRADE_MSG = "This file exceeds the Free tier limit (
|
|
115
|
+
const PRO_UPGRADE_MSG = "This file exceeds the Free tier limit (200 files). Upgrade to Pro for unlimited analysis: https://syke.cloud/dashboard/";
|
|
102
116
|
function getProToolError(toolName) {
|
|
103
117
|
if (licenseStatus.error) {
|
|
104
118
|
return `${toolName}: ${licenseStatus.error}`;
|
|
@@ -133,7 +147,7 @@ async function main() {
|
|
|
133
147
|
};
|
|
134
148
|
process.on("SIGINT", shutdown);
|
|
135
149
|
process.on("SIGTERM", shutdown);
|
|
136
|
-
const server = new index_js_1.Server({ name: "syke", version: "1.
|
|
150
|
+
const server = new index_js_1.Server({ name: "syke", version: "1.8.1" }, { capabilities: { tools: {} } });
|
|
137
151
|
// List tools
|
|
138
152
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
139
153
|
tools: [
|
|
@@ -213,7 +227,7 @@ async function main() {
|
|
|
213
227
|
},
|
|
214
228
|
{
|
|
215
229
|
name: "ai_analyze",
|
|
216
|
-
description: "[
|
|
230
|
+
description: "[CORTEX] Use AI (Gemini/OpenAI/Claude) to perform semantic analysis on a file. Reads the file's source code and its dependents to explain what might break when modified and how to safely make changes.",
|
|
217
231
|
inputSchema: {
|
|
218
232
|
type: "object",
|
|
219
233
|
properties: {
|
|
@@ -238,6 +252,16 @@ async function main() {
|
|
|
238
252
|
},
|
|
239
253
|
},
|
|
240
254
|
},
|
|
255
|
+
{
|
|
256
|
+
name: "scan_project",
|
|
257
|
+
description: "[CORTEX] Scan the entire project and generate a comprehensive onboarding document. "
|
|
258
|
+
+ "Analyzes architecture, key files, dependencies, and patterns to help new team members "
|
|
259
|
+
+ "understand the codebase quickly. Requires AI API key (BYOK).",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
241
265
|
],
|
|
242
266
|
}));
|
|
243
267
|
// Dashboard URL footer — shown only on the first successful tool call
|
|
@@ -491,11 +515,16 @@ async function main() {
|
|
|
491
515
|
};
|
|
492
516
|
}
|
|
493
517
|
case "ai_analyze": {
|
|
494
|
-
//
|
|
518
|
+
// Cortex-only tool
|
|
519
|
+
if (!isCortex()) {
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: "text", text: getCortexToolError("ai_analyze") }],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
495
524
|
const hasAIKey = !!(0, provider_1.getAIProvider)();
|
|
496
|
-
if (!
|
|
525
|
+
if (!hasAIKey) {
|
|
497
526
|
return {
|
|
498
|
-
content: [{ type: "text", text:
|
|
527
|
+
content: [{ type: "text", text: `ai_analyze requires an AI API key.\n\nSet one of: GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY in your environment.\nSee: https://syke.cloud/docs/ai-analyze` }],
|
|
499
528
|
};
|
|
500
529
|
}
|
|
501
530
|
const file = args.file;
|
|
@@ -511,24 +540,11 @@ async function main() {
|
|
|
511
540
|
],
|
|
512
541
|
};
|
|
513
542
|
}
|
|
514
|
-
if (!isFileInFreeSet(resolved, graph)) {
|
|
515
|
-
return { content: [{ type: "text", text: PRO_UPGRADE_MSG }] };
|
|
516
|
-
}
|
|
517
|
-
// BYOK only: user must have their own AI key configured
|
|
518
|
-
if (!hasAIKey) {
|
|
519
|
-
return {
|
|
520
|
-
content: [{ type: "text", text: `ai_analyze requires an AI API key.\n\nSet one of: GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY in your environment.\nSee: https://syke.cloud/docs/ai-analyze` }],
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
543
|
const { analyzeImpact: localAnalyzeImpact } = await Promise.resolve().then(() => __importStar(require("./tools/analyze-impact")));
|
|
524
544
|
const impactResult = await localAnalyzeImpact(resolved, graph);
|
|
525
545
|
const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
|
|
526
|
-
let resultText = aiResult;
|
|
527
|
-
if (!isPro() && graph.files.size > FREE_MAX_FILES) {
|
|
528
|
-
resultText += `\n\n---\nFree tier: analyzing ${FREE_MAX_FILES}/${graph.files.size} files. Some dependencies may be missing. Upgrade to Pro for full analysis: https://syke.cloud/dashboard/`;
|
|
529
|
-
}
|
|
530
546
|
return {
|
|
531
|
-
content: [{ type: "text", text: appendDashboardFooter(
|
|
547
|
+
content: [{ type: "text", text: appendDashboardFooter(aiResult) }],
|
|
532
548
|
};
|
|
533
549
|
}
|
|
534
550
|
case "check_warnings": {
|
|
@@ -582,6 +598,26 @@ async function main() {
|
|
|
582
598
|
content: [{ type: "text", text: appendDashboardFooter(lines.join("\n")) }],
|
|
583
599
|
};
|
|
584
600
|
}
|
|
601
|
+
case "scan_project": {
|
|
602
|
+
// Cortex-only tool
|
|
603
|
+
if (!isCortex()) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: getCortexToolError("scan_project") }],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const hasAIKey = !!(0, provider_1.getAIProvider)();
|
|
609
|
+
if (!hasAIKey) {
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: `scan_project requires an AI API key.\n\nSet one of: GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY in your environment.\nSee: https://syke.cloud/docs/ai-analyze` }],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const graph = (0, graph_1.getGraph)(currentProjectRoot, currentPackageName, getMaxFiles());
|
|
615
|
+
const { scanProject } = await Promise.resolve().then(() => __importStar(require("./ai/project-scanner")));
|
|
616
|
+
const result = await scanProject(graph);
|
|
617
|
+
return {
|
|
618
|
+
content: [{ type: "text", text: appendDashboardFooter(result) }],
|
|
619
|
+
};
|
|
620
|
+
}
|
|
585
621
|
default:
|
|
586
622
|
return {
|
|
587
623
|
content: [
|
|
@@ -592,7 +628,7 @@ async function main() {
|
|
|
592
628
|
}
|
|
593
629
|
});
|
|
594
630
|
// Pre-warm the graph (skip if no project root — e.g. Smithery scan)
|
|
595
|
-
console.error(`[syke] Starting SYKE MCP Server v1.
|
|
631
|
+
console.error(`[syke] Starting SYKE MCP Server v1.8.1`);
|
|
596
632
|
console.error(`[syke] License: ${licenseStatus.plan.toUpperCase()} (${licenseStatus.source})`);
|
|
597
633
|
if (licenseStatus.expiresAt) {
|
|
598
634
|
console.error(`[syke] Expires: ${licenseStatus.expiresAt}`);
|
|
@@ -762,7 +798,7 @@ main().catch((err) => {
|
|
|
762
798
|
* See: https://smithery.ai/docs/deploy#sandbox-server
|
|
763
799
|
*/
|
|
764
800
|
function createSandboxServer() {
|
|
765
|
-
const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.
|
|
801
|
+
const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.8.1" }, { capabilities: { tools: {} } });
|
|
766
802
|
sandboxServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
767
803
|
tools: [
|
|
768
804
|
{
|
|
@@ -797,7 +833,7 @@ function createSandboxServer() {
|
|
|
797
833
|
},
|
|
798
834
|
{
|
|
799
835
|
name: "ai_analyze",
|
|
800
|
-
description: "
|
|
836
|
+
description: "Cortex: AI semantic analysis (Gemini/OpenAI/Claude) of a file and its dependents.",
|
|
801
837
|
inputSchema: { type: "object", properties: { file: { type: "string", description: "File to analyze" } }, required: ["file"] },
|
|
802
838
|
},
|
|
803
839
|
{
|
|
@@ -805,6 +841,11 @@ function createSandboxServer() {
|
|
|
805
841
|
description: "Pro: Real-time monitoring alerts for file changes.",
|
|
806
842
|
inputSchema: { type: "object", properties: {} },
|
|
807
843
|
},
|
|
844
|
+
{
|
|
845
|
+
name: "scan_project",
|
|
846
|
+
description: "Cortex: Scan entire project and generate onboarding document.",
|
|
847
|
+
inputSchema: { type: "object", properties: {} },
|
|
848
|
+
},
|
|
808
849
|
],
|
|
809
850
|
}));
|
|
810
851
|
sandboxServer.setRequestHandler(types_js_1.CallToolRequestSchema, async () => ({
|
|
@@ -245,6 +245,17 @@ function clearLicenseCache() {
|
|
|
245
245
|
// silently fail
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Map server plan string to client plan type.
|
|
250
|
+
* Cortex plans → "cortex", trial → "pro_trial", everything else paid → "pro".
|
|
251
|
+
*/
|
|
252
|
+
function mapPlan(serverPlan) {
|
|
253
|
+
if (serverPlan === "pro_trial")
|
|
254
|
+
return "pro_trial";
|
|
255
|
+
if (serverPlan === "cortex_monthly" || serverPlan === "cortex_annual" || serverPlan === "cortex")
|
|
256
|
+
return "cortex";
|
|
257
|
+
return "pro"; // pro_monthly, pro_annual, pro_founding, master, etc.
|
|
258
|
+
}
|
|
248
259
|
/**
|
|
249
260
|
* Main license validation — called on MCP server startup
|
|
250
261
|
*/
|
|
@@ -260,7 +271,7 @@ async function checkLicense() {
|
|
|
260
271
|
if (cache && cache.valid && (now - cache.cachedAt) < CACHE_MAX_AGE) {
|
|
261
272
|
// Cache is fresh — still start heartbeat with cached session
|
|
262
273
|
startHeartbeat(key, getDeviceFingerprint());
|
|
263
|
-
const cachedPlan = cache.plan
|
|
274
|
+
const cachedPlan = mapPlan(cache.plan || "");
|
|
264
275
|
return {
|
|
265
276
|
plan: cachedPlan,
|
|
266
277
|
email: cache.email,
|
|
@@ -285,12 +296,13 @@ async function checkLicense() {
|
|
|
285
296
|
cachedAt: now,
|
|
286
297
|
});
|
|
287
298
|
startHeartbeat(realKey, getDeviceFingerprint());
|
|
299
|
+
const foundingPlan = mapPlan(result.plan || "");
|
|
288
300
|
return {
|
|
289
|
-
plan:
|
|
301
|
+
plan: foundingPlan,
|
|
290
302
|
email: result.email,
|
|
291
303
|
expiresAt: result.expiresAt,
|
|
292
304
|
source: "online",
|
|
293
|
-
_t: _signStatus(
|
|
305
|
+
_t: _signStatus(foundingPlan, "online"),
|
|
294
306
|
};
|
|
295
307
|
}
|
|
296
308
|
if (!result.valid) {
|
|
@@ -308,7 +320,7 @@ async function checkLicense() {
|
|
|
308
320
|
});
|
|
309
321
|
// Start heartbeat
|
|
310
322
|
startHeartbeat(key, getDeviceFingerprint());
|
|
311
|
-
const onlinePlan = result.plan
|
|
323
|
+
const onlinePlan = mapPlan(result.plan || "");
|
|
312
324
|
return {
|
|
313
325
|
plan: onlinePlan,
|
|
314
326
|
email: result.email,
|
|
@@ -329,7 +341,7 @@ async function checkLicense() {
|
|
|
329
341
|
// Online validation failed — check if we have a grace-period cache
|
|
330
342
|
if (cache && cache.valid && (now - cache.cachedAt) < CACHE_GRACE_PERIOD) {
|
|
331
343
|
startHeartbeat(key, getDeviceFingerprint());
|
|
332
|
-
const gracePlan = cache.plan
|
|
344
|
+
const gracePlan = mapPlan(cache.plan || "");
|
|
333
345
|
return {
|
|
334
346
|
plan: gracePlan,
|
|
335
347
|
email: cache.email,
|
package/dist/web/server.js
CHANGED
|
@@ -408,7 +408,7 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
408
408
|
app.get("/api/graph", (_req, res) => {
|
|
409
409
|
const graph = getGraphFn();
|
|
410
410
|
const isPro = isProPlan();
|
|
411
|
-
const FREE_GRAPH_LIMIT =
|
|
411
|
+
const FREE_GRAPH_LIMIT = 200;
|
|
412
412
|
const nodes = [];
|
|
413
413
|
const edges = [];
|
|
414
414
|
// ── Compute depth for each file (BFS from roots) ──
|
|
@@ -429,7 +429,7 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
429
429
|
queue.push([dep, d + 1]);
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
|
-
// Free tier: limit to first
|
|
432
|
+
// Free tier: limit to first 200 files sorted alphabetically
|
|
433
433
|
const allFiles = [...graph.files].sort();
|
|
434
434
|
const visibleFiles = isPro ? allFiles : allFiles.slice(0, FREE_GRAPH_LIMIT);
|
|
435
435
|
const visibleSet = new Set(visibleFiles);
|
|
@@ -521,14 +521,14 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
521
521
|
if (!isPro) {
|
|
522
522
|
const allFiles = [...graph.files].sort();
|
|
523
523
|
const idx = allFiles.indexOf(resolved);
|
|
524
|
-
if (idx < 0 || idx >=
|
|
525
|
-
return res.status(403).json({ error: "This file exceeds the Free tier limit (
|
|
524
|
+
if (idx < 0 || idx >= 200) {
|
|
525
|
+
return res.status(403).json({ error: "This file exceeds the Free tier limit (200 files). Upgrade to Pro for unlimited analysis.", upgrade: "https://syke.cloud/dashboard/" });
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
const impactResult = await (0, analyze_impact_1.analyzeImpact)(resolved, graph);
|
|
529
529
|
try {
|
|
530
530
|
const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
|
|
531
|
-
const partial = !isPro && graph.files.size >
|
|
531
|
+
const partial = !isPro && graph.files.size > 200;
|
|
532
532
|
res.json({ file: impactResult.relativePath, analysis: aiResult, partial });
|
|
533
533
|
}
|
|
534
534
|
catch (err) {
|
|
@@ -792,7 +792,7 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
792
792
|
planSource: license?.source || "default",
|
|
793
793
|
expiresAt: license?.expiresAt || null,
|
|
794
794
|
licenseKey: maskedKey,
|
|
795
|
-
freeFileLimit:
|
|
795
|
+
freeFileLimit: 200,
|
|
796
796
|
sykeVersion,
|
|
797
797
|
aiProvider: aiInfo.activeProvider,
|
|
798
798
|
aiKeys: aiInfo.configured,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syke1/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"mcpName": "io.github.khalomsky/syke",
|
|
5
5
|
"description": "AI code impact analysis MCP server — dependency graphs, cascade detection, and a mandatory build gate for AI coding agents",
|
|
6
6
|
"main": "dist/index.js",
|