@mcptoolshop/claude-synergy 0.0.0 → 1.0.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/CHANGELOG.md +126 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +21 -0
- package/README.es.md +340 -0
- package/README.fr.md +340 -0
- package/README.hi.md +340 -0
- package/README.it.md +340 -0
- package/README.ja.md +340 -0
- package/README.md +337 -5
- package/README.pt-BR.md +340 -0
- package/README.zh.md +340 -0
- package/dist/chunk-HCIZPSW4.js +469 -0
- package/dist/chunk-YFGUTT22.js +754 -0
- package/dist/cli.js +2090 -0
- package/dist/fetch-playwright-HQ6OTMSQ.js +80 -0
- package/dist/ingest-3LJNQWS7.js +6 -0
- package/dist/mcp-server.js +497 -0
- package/package.json +81 -7
- package/products.yaml +456 -0
- package/schema-vec.sql +43 -0
- package/schema.sql +155 -0
- package/synergies/01-skill-portability.md +33 -0
- package/synergies/02-mcp-server-portability.md +33 -0
- package/synergies/03-design-to-code-bundle.md +33 -0
- package/synergies/04-computer-use-cross-surface.md +33 -0
- package/synergies/05-ollama-cost-shifting.md +36 -0
- package/synergies/06-agent-sdk-graduation.md +34 -0
- package/synergies/07-code-review-in-ci.md +33 -0
- package/synergies/08-universal-skill-md-format.md +46 -0
- package/synergies/09-mcp-across-seven-surfaces.md +48 -0
- package/synergies/10-anthropic-byok-across-surfaces.md +46 -0
- package/synergies/11-claude-code-orchestrates-aider.md +52 -0
- package/synergies/12-mcp-config-format-gotcha.md +60 -0
- package/synergies/INDEX.md +47 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/fetch-playwright.ts
|
|
2
|
+
async function fetchWindsurfWithPlaywright(sinceIso, opts = {}) {
|
|
3
|
+
let chromium;
|
|
4
|
+
try {
|
|
5
|
+
({ chromium } = await import("playwright"));
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`[fetch-playwright] playwright is not installed. To enable Windsurf coverage, run: pnpm add -D playwright && npx playwright install chromium`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
12
|
+
const url = "https://windsurf.com/changelog";
|
|
13
|
+
const browser = await chromium.launch({ headless: true });
|
|
14
|
+
try {
|
|
15
|
+
const context = await browser.newContext({
|
|
16
|
+
userAgent: "claude-synergy/0.1.0 (+https://github.com/mcp-tool-shop-org/claude-synergy)"
|
|
17
|
+
});
|
|
18
|
+
const page = await context.newPage();
|
|
19
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: timeoutMs });
|
|
20
|
+
await page.waitForSelector("h1[id], h2[id], h3[id]", { timeout: timeoutMs });
|
|
21
|
+
const rawEntries = await page.evaluate(() => {
|
|
22
|
+
const entries = [];
|
|
23
|
+
const headings = Array.from(document.querySelectorAll("h1[id]"));
|
|
24
|
+
for (let i = 0; i < headings.length; i++) {
|
|
25
|
+
const head = headings[i];
|
|
26
|
+
const id = head.id;
|
|
27
|
+
const title = head.textContent?.trim() ?? "";
|
|
28
|
+
if (!title || !id) continue;
|
|
29
|
+
let date = null;
|
|
30
|
+
let wrapper = head;
|
|
31
|
+
for (let depth = 0; depth < 6 && wrapper; depth++) {
|
|
32
|
+
const sample = (wrapper.textContent ?? "").slice(0, 200);
|
|
33
|
+
const m = sample.match(/(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i);
|
|
34
|
+
if (m) {
|
|
35
|
+
const d = new Date(m[0]);
|
|
36
|
+
if (!Number.isNaN(d.getTime())) {
|
|
37
|
+
date = d.toISOString();
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
wrapper = wrapper.parentElement;
|
|
42
|
+
}
|
|
43
|
+
const bodyParts = [];
|
|
44
|
+
let next = head.nextElementSibling;
|
|
45
|
+
const nextHead = headings[i + 1];
|
|
46
|
+
while (next && next !== nextHead) {
|
|
47
|
+
if (next instanceof HTMLElement && next.matches("h1[id]")) break;
|
|
48
|
+
bodyParts.push(next.outerHTML);
|
|
49
|
+
next = next.nextElementSibling;
|
|
50
|
+
}
|
|
51
|
+
entries.push({
|
|
52
|
+
title,
|
|
53
|
+
id,
|
|
54
|
+
date,
|
|
55
|
+
body: bodyParts.join("\n").trim()
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
});
|
|
60
|
+
const items = [];
|
|
61
|
+
for (const e of rawEntries) {
|
|
62
|
+
const pubDate = e.date ?? null;
|
|
63
|
+
if (!pubDate) continue;
|
|
64
|
+
if (pubDate <= sinceIso) continue;
|
|
65
|
+
items.push({
|
|
66
|
+
slug: `${pubDate.split("T")[0]}-${e.id}`,
|
|
67
|
+
title: e.title,
|
|
68
|
+
pubDate,
|
|
69
|
+
link: `${url}#${e.id}`,
|
|
70
|
+
body: e.body
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
} finally {
|
|
75
|
+
await browser.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
fetchWindsurfWithPlaywright
|
|
80
|
+
};
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
entityFrequency,
|
|
4
|
+
hybridSearch,
|
|
5
|
+
listProducts,
|
|
6
|
+
lookupEntity,
|
|
7
|
+
openDb,
|
|
8
|
+
recentReleases,
|
|
9
|
+
searchChanges
|
|
10
|
+
} from "./chunk-YFGUTT22.js";
|
|
11
|
+
|
|
12
|
+
// src/mcp-server.ts
|
|
13
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import {
|
|
16
|
+
CallToolRequestSchema,
|
|
17
|
+
ListToolsRequestSchema,
|
|
18
|
+
ErrorCode,
|
|
19
|
+
McpError
|
|
20
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
22
|
+
import { join, dirname, basename } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import { createRequire } from "module";
|
|
25
|
+
import matter from "gray-matter";
|
|
26
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
var _require = createRequire(import.meta.url);
|
|
28
|
+
var { version: PKG_VERSION } = _require("../package.json");
|
|
29
|
+
var SYNERGY_NAME_RE = /^[a-z0-9_-]+$/i;
|
|
30
|
+
var DB_PATH = process.env.CLAUDE_SYNERGY_DB ?? process.argv[2] ?? join(process.cwd(), "data", "claude-synergy.db");
|
|
31
|
+
var SYNERGIES_DIR = process.env.CLAUDE_SYNERGY_SYNERGIES_DIR ?? resolveSynergiesDir();
|
|
32
|
+
function resolveSynergiesDir() {
|
|
33
|
+
const dbDir = dirname(DB_PATH);
|
|
34
|
+
const candidates = [
|
|
35
|
+
join(dbDir, "..", "synergies"),
|
|
36
|
+
join(process.cwd(), "synergies"),
|
|
37
|
+
join(__dirname, "..", "synergies")
|
|
38
|
+
];
|
|
39
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
40
|
+
return candidates[0];
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(DB_PATH)) {
|
|
43
|
+
console.error(
|
|
44
|
+
`claude-synergy-mcp: DB not found at ${DB_PATH}. Run 'hk init && hk ingest && hk embed' first, or set CLAUDE_SYNERGY_DB.`
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
var db = openDb(DB_PATH);
|
|
49
|
+
var hasChunks = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get() !== void 0;
|
|
50
|
+
var server = new Server(
|
|
51
|
+
{ name: "claude-synergy", version: PKG_VERSION },
|
|
52
|
+
{ capabilities: { tools: {} } }
|
|
53
|
+
);
|
|
54
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
55
|
+
tools: [
|
|
56
|
+
{
|
|
57
|
+
name: "search",
|
|
58
|
+
description: "Search the Anthropic product changelog corpus. Use mode=hybrid (default, requires embeddings) for semantic/conceptual queries; mode=fts for exact keyword/phrase matching. Returns ranked changelog bullets with product+version+date metadata.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
query: { type: "string", description: "Search query. Natural language works for hybrid; FTS5 syntax for fts mode." },
|
|
63
|
+
mode: { type: "string", enum: ["hybrid", "fts"], description: "hybrid = FTS5+vec via RRF (best for concepts); fts = BM25 only (best for exact terms)", default: "hybrid" },
|
|
64
|
+
product: { type: "string", description: "Limit to one product (e.g. claude-code, claude-agent-sdk-python, anthropic-cli)" },
|
|
65
|
+
since: { type: "string", description: "YYYY-MM-DD lower bound on release date" },
|
|
66
|
+
kind: { type: "string", description: "Filter by change kind: added | fixed | breaking | deprecated | renamed | removed | improved | changed" },
|
|
67
|
+
rerank: { type: "string", enum: ["none", "ollama-judge"], description: "Rerank top-K candidates (hybrid mode only). Defaults to none for speed.", default: "none" },
|
|
68
|
+
limit: { type: "number", description: "Max results (default 10)", default: 10 }
|
|
69
|
+
},
|
|
70
|
+
required: ["query"]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "lookup_entity",
|
|
75
|
+
description: "Find every release that mentions a specific entity: env var, slash command, CLI option, model ID, beta header, hook event, CVE, GHSA. Returns chronological history of mentions across products.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
type: {
|
|
80
|
+
type: "string",
|
|
81
|
+
enum: ["env_var", "slash_command", "cli_option", "model_id", "beta_header", "hook_event", "setting_key", "cve", "ghsa"],
|
|
82
|
+
description: "Entity type"
|
|
83
|
+
},
|
|
84
|
+
value: { type: "string", description: "Exact entity value (e.g. CLAUDE_CODE_WORKFLOWS, /code-review, claude-opus-4-7, CVE-2025-66414)" }
|
|
85
|
+
},
|
|
86
|
+
required: ["type", "value"]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "latest_releases",
|
|
91
|
+
description: "Get recent releases across all products (or one). Use this to orient on what shipped recently before recommending features.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
product: { type: "string", description: "Limit to one product" },
|
|
96
|
+
limit: { type: "number", description: "Max releases", default: 20 }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_release",
|
|
102
|
+
description: "Full content of one specific release (all change bullets + metadata).",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
product: { type: "string" },
|
|
107
|
+
version: { type: "string" }
|
|
108
|
+
},
|
|
109
|
+
required: ["product", "version"]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "list_products",
|
|
114
|
+
description: "Enumerate products in the database with release counts and date of latest release. Use this for orientation.",
|
|
115
|
+
inputSchema: { type: "object", properties: {} }
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "top_entities",
|
|
119
|
+
description: 'Most-mentioned entities of a given type (e.g. top env vars, top slash commands). Useful for "what env vars exist in Claude Code".',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
type: {
|
|
124
|
+
type: "string",
|
|
125
|
+
enum: ["env_var", "slash_command", "cli_option", "model_id", "beta_header", "hook_event", "setting_key", "cve", "ghsa"]
|
|
126
|
+
},
|
|
127
|
+
limit: { type: "number", default: 30 }
|
|
128
|
+
},
|
|
129
|
+
required: ["type"]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "list_synergies",
|
|
134
|
+
description: "List curated cross-product workflows (Claude Design \u2194 Code bundle, MCP server portability, etc). Each synergy describes a composition pattern with evidence.",
|
|
135
|
+
inputSchema: { type: "object", properties: {} }
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "read_synergy",
|
|
139
|
+
description: "Read one synergy file in full. Use after list_synergies to drill into a specific pattern.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: { name: { type: "string", description: "Synergy name from list_synergies (e.g. skill-portability)" } },
|
|
143
|
+
required: ["name"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}));
|
|
148
|
+
var SEARCH_MODES = /* @__PURE__ */ new Set(["hybrid", "fts"]);
|
|
149
|
+
var RERANK_MODES = /* @__PURE__ */ new Set(["none", "ollama-judge"]);
|
|
150
|
+
var ENTITY_TYPES = /* @__PURE__ */ new Set([
|
|
151
|
+
"env_var",
|
|
152
|
+
"slash_command",
|
|
153
|
+
"cli_option",
|
|
154
|
+
"model_id",
|
|
155
|
+
"beta_header",
|
|
156
|
+
"hook_event",
|
|
157
|
+
"setting_key",
|
|
158
|
+
"cve",
|
|
159
|
+
"ghsa"
|
|
160
|
+
]);
|
|
161
|
+
function asRecord(args, tool) {
|
|
162
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) {
|
|
163
|
+
throw new McpError(ErrorCode.InvalidParams, `${tool}: arguments must be an object`);
|
|
164
|
+
}
|
|
165
|
+
return args;
|
|
166
|
+
}
|
|
167
|
+
function requireString(rec, field, tool) {
|
|
168
|
+
const v = rec[field];
|
|
169
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
170
|
+
throw new McpError(ErrorCode.InvalidParams, `${tool}: ${field} must be a non-empty string`);
|
|
171
|
+
}
|
|
172
|
+
return v;
|
|
173
|
+
}
|
|
174
|
+
function optString(rec, field, tool) {
|
|
175
|
+
const v = rec[field];
|
|
176
|
+
if (v === void 0 || v === null) return void 0;
|
|
177
|
+
if (typeof v !== "string") {
|
|
178
|
+
throw new McpError(ErrorCode.InvalidParams, `${tool}: ${field} must be a string`);
|
|
179
|
+
}
|
|
180
|
+
return v;
|
|
181
|
+
}
|
|
182
|
+
function optInt(rec, field, tool, min = 1, max = 1e4) {
|
|
183
|
+
const v = rec[field];
|
|
184
|
+
if (v === void 0 || v === null) return void 0;
|
|
185
|
+
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v) || v < min || v > max) {
|
|
186
|
+
throw new McpError(
|
|
187
|
+
ErrorCode.InvalidParams,
|
|
188
|
+
`${tool}: ${field} must be an integer in [${min}, ${max}]`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return v;
|
|
192
|
+
}
|
|
193
|
+
function optEnum(rec, field, allowed, tool) {
|
|
194
|
+
const v = rec[field];
|
|
195
|
+
if (v === void 0 || v === null) return void 0;
|
|
196
|
+
if (typeof v !== "string" || !allowed.has(v)) {
|
|
197
|
+
throw new McpError(
|
|
198
|
+
ErrorCode.InvalidParams,
|
|
199
|
+
`${tool}: ${field} must be one of ${[...allowed].join(", ")}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
204
|
+
var SEARCH_TIMEOUT_MS = 3e4;
|
|
205
|
+
function withTimeout(promise, ms, message) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const timer = setTimeout(() => reject(new McpError(ErrorCode.InternalError, message)), ms);
|
|
208
|
+
promise.then(
|
|
209
|
+
(val) => {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
resolve(val);
|
|
212
|
+
},
|
|
213
|
+
(err) => {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
reject(err);
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
221
|
+
const { name, arguments: args } = request.params;
|
|
222
|
+
try {
|
|
223
|
+
switch (name) {
|
|
224
|
+
case "search": {
|
|
225
|
+
const r = asRecord(args, "search");
|
|
226
|
+
const searchResult = await withTimeout(
|
|
227
|
+
handleSearch({
|
|
228
|
+
query: requireString(r, "query", "search"),
|
|
229
|
+
mode: optEnum(r, "mode", SEARCH_MODES, "search"),
|
|
230
|
+
product: optString(r, "product", "search"),
|
|
231
|
+
since: optString(r, "since", "search"),
|
|
232
|
+
kind: optString(r, "kind", "search"),
|
|
233
|
+
rerank: optEnum(r, "rerank", RERANK_MODES, "search"),
|
|
234
|
+
limit: optInt(r, "limit", "search")
|
|
235
|
+
}),
|
|
236
|
+
SEARCH_TIMEOUT_MS,
|
|
237
|
+
"search timed out (30s) \u2014 is the embedding provider (Ollama) running?"
|
|
238
|
+
);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: searchResult }]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case "lookup_entity": {
|
|
244
|
+
const r = asRecord(args, "lookup_entity");
|
|
245
|
+
const type = requireString(r, "type", "lookup_entity");
|
|
246
|
+
if (!ENTITY_TYPES.has(type)) {
|
|
247
|
+
throw new McpError(
|
|
248
|
+
ErrorCode.InvalidParams,
|
|
249
|
+
`lookup_entity: type must be one of ${[...ENTITY_TYPES].join(", ")}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: handleLookupEntity({ type, value: requireString(r, "value", "lookup_entity") })
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
case "latest_releases": {
|
|
262
|
+
const r = asRecord(args ?? {}, "latest_releases");
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: handleLatestReleases({
|
|
268
|
+
product: optString(r, "product", "latest_releases"),
|
|
269
|
+
limit: optInt(r, "limit", "latest_releases")
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
case "get_release": {
|
|
276
|
+
const r = asRecord(args, "get_release");
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: handleGetRelease({
|
|
282
|
+
product: requireString(r, "product", "get_release"),
|
|
283
|
+
version: requireString(r, "version", "get_release")
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
case "list_products":
|
|
290
|
+
return { content: [{ type: "text", text: handleListProducts() }] };
|
|
291
|
+
case "top_entities": {
|
|
292
|
+
const r = asRecord(args, "top_entities");
|
|
293
|
+
const type = requireString(r, "type", "top_entities");
|
|
294
|
+
if (!ENTITY_TYPES.has(type)) {
|
|
295
|
+
throw new McpError(
|
|
296
|
+
ErrorCode.InvalidParams,
|
|
297
|
+
`top_entities: type must be one of ${[...ENTITY_TYPES].join(", ")}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: handleTopEntities({ type, limit: optInt(r, "limit", "top_entities") })
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case "list_synergies":
|
|
310
|
+
return { content: [{ type: "text", text: handleListSynergies() }] };
|
|
311
|
+
case "read_synergy": {
|
|
312
|
+
const r = asRecord(args, "read_synergy");
|
|
313
|
+
const synName = requireString(r, "name", "read_synergy");
|
|
314
|
+
if (!SYNERGY_NAME_RE.test(synName)) {
|
|
315
|
+
throw new McpError(
|
|
316
|
+
ErrorCode.InvalidParams,
|
|
317
|
+
"read_synergy: name must match /^[a-z0-9_-]+$/i"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return { content: [{ type: "text", text: handleReadSynergy({ name: synName }) }] };
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
if (e instanceof McpError) throw e;
|
|
327
|
+
throw new McpError(ErrorCode.InternalError, e.message);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
async function handleSearch(args) {
|
|
331
|
+
const mode = args.mode ?? "hybrid";
|
|
332
|
+
const limit = args.limit ?? 10;
|
|
333
|
+
if (mode === "fts") {
|
|
334
|
+
const results2 = searchChanges(db, args.query, {
|
|
335
|
+
product: args.product,
|
|
336
|
+
since: args.since,
|
|
337
|
+
kind: args.kind,
|
|
338
|
+
limit
|
|
339
|
+
});
|
|
340
|
+
return formatSearchResults(results2.map((r) => ({
|
|
341
|
+
product: r.product,
|
|
342
|
+
version: r.version,
|
|
343
|
+
released_at: r.released_at,
|
|
344
|
+
kind: r.kind,
|
|
345
|
+
text: r.text,
|
|
346
|
+
mode: "fts"
|
|
347
|
+
})));
|
|
348
|
+
}
|
|
349
|
+
if (!hasChunks) {
|
|
350
|
+
throw new McpError(
|
|
351
|
+
ErrorCode.InvalidParams,
|
|
352
|
+
'hybrid mode requires embeddings \u2014 run `hk embed` first, or pass mode="fts"'
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const results = await hybridSearch(db, args.query, {
|
|
356
|
+
product: args.product,
|
|
357
|
+
since: args.since,
|
|
358
|
+
kind: args.kind,
|
|
359
|
+
rerankProviderName: args.rerank ?? "none",
|
|
360
|
+
limit
|
|
361
|
+
});
|
|
362
|
+
return formatSearchResults(results.map((r) => ({
|
|
363
|
+
product: r.product,
|
|
364
|
+
version: r.version,
|
|
365
|
+
released_at: r.released_at,
|
|
366
|
+
kind: r.kind,
|
|
367
|
+
text: r.text,
|
|
368
|
+
mode: "hybrid",
|
|
369
|
+
rrf_score: r.rrf_score,
|
|
370
|
+
rerank_score: r.rerank_score
|
|
371
|
+
})));
|
|
372
|
+
}
|
|
373
|
+
function formatSearchResults(results) {
|
|
374
|
+
if (results.length === 0) return "(no results)";
|
|
375
|
+
const lines = [];
|
|
376
|
+
for (const r of results) {
|
|
377
|
+
const scoreInfo = r.rerank_score != null ? ` rerank=${r.rerank_score.toFixed(2)}` : r.rrf_score != null ? ` rrf=${r.rrf_score.toFixed(4)}` : "";
|
|
378
|
+
lines.push(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]${scoreInfo}`);
|
|
379
|
+
lines.push(` ${r.text}`);
|
|
380
|
+
lines.push("");
|
|
381
|
+
}
|
|
382
|
+
lines.push(`${results.length} result${results.length === 1 ? "" : "s"}`);
|
|
383
|
+
return lines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
function handleLookupEntity(args) {
|
|
386
|
+
const results = lookupEntity(db, args.type, args.value);
|
|
387
|
+
if (results.length === 0) return `(no mentions of ${args.type}: ${args.value})`;
|
|
388
|
+
const lines = [`${args.type} ${args.value} \u2014 ${results.length} mention${results.length === 1 ? "" : "s"}:
|
|
389
|
+
`];
|
|
390
|
+
for (const r of results) {
|
|
391
|
+
lines.push(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]`);
|
|
392
|
+
lines.push(` ${r.text}`);
|
|
393
|
+
lines.push("");
|
|
394
|
+
}
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
function handleLatestReleases(args) {
|
|
398
|
+
const limit = args.limit ?? 20;
|
|
399
|
+
const releases = recentReleases(db, args.product, limit);
|
|
400
|
+
if (releases.length === 0) return "(no releases)";
|
|
401
|
+
return releases.map((r) => `${r.released_at} ${r.product}@${r.version} (${r.change_count} change${r.change_count === 1 ? "" : "s"})`).join("\n");
|
|
402
|
+
}
|
|
403
|
+
function handleGetRelease(args) {
|
|
404
|
+
const release = db.prepare(`SELECT product, version, released_at, source_url, notes_path FROM releases WHERE product = ? AND version = ?`).get(args.product, args.version);
|
|
405
|
+
if (!release) return `(no such release: ${args.product}@${args.version})`;
|
|
406
|
+
const changes = db.prepare(`SELECT ordinal, kind, text FROM changes WHERE product = ? AND version = ? ORDER BY ordinal`).all(args.product, args.version);
|
|
407
|
+
const lines = [
|
|
408
|
+
`# ${release.product} ${release.version}`,
|
|
409
|
+
`Released: ${release.released_at}`,
|
|
410
|
+
`Source: ${release.source_url}`,
|
|
411
|
+
`Notes file: ${release.notes_path}`,
|
|
412
|
+
``,
|
|
413
|
+
`## Changes (${changes.length})`,
|
|
414
|
+
``
|
|
415
|
+
];
|
|
416
|
+
for (const c of changes) {
|
|
417
|
+
lines.push(`- [${c.kind}] ${c.text}`);
|
|
418
|
+
}
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
}
|
|
421
|
+
function handleListProducts() {
|
|
422
|
+
const products = listProducts(db);
|
|
423
|
+
if (products.length === 0) return "(no products)";
|
|
424
|
+
const lines = [
|
|
425
|
+
"Product Releases Latest",
|
|
426
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
427
|
+
];
|
|
428
|
+
for (const p of products) {
|
|
429
|
+
const latest = p.latest_version ? `${p.latest_version} (${p.latest_date})` : "\u2014";
|
|
430
|
+
lines.push(`${p.name.padEnd(36)} ${String(p.release_count).padStart(8)} ${latest}`);
|
|
431
|
+
}
|
|
432
|
+
return lines.join("\n");
|
|
433
|
+
}
|
|
434
|
+
function handleTopEntities(args) {
|
|
435
|
+
const results = entityFrequency(db, args.type, args.limit ?? 30);
|
|
436
|
+
if (results.length === 0) return `(no entities of type ${args.type})`;
|
|
437
|
+
return results.map((r) => `${String(r.count).padStart(4)} ${r.first_seen ?? "????-??-??"} ${r.value}`).join("\n");
|
|
438
|
+
}
|
|
439
|
+
function handleListSynergies() {
|
|
440
|
+
if (!existsSync(SYNERGIES_DIR)) return "(synergies dir not found)";
|
|
441
|
+
const files = readdirSync(SYNERGIES_DIR).filter((f) => f.endsWith(".md") && f !== "INDEX.md");
|
|
442
|
+
if (files.length === 0) return "(no synergies)";
|
|
443
|
+
const lines = [`Synergies (${files.length}):
|
|
444
|
+
`];
|
|
445
|
+
for (const f of files) {
|
|
446
|
+
try {
|
|
447
|
+
const raw = readFileSync(join(SYNERGIES_DIR, f), "utf-8");
|
|
448
|
+
const { data } = matter(raw);
|
|
449
|
+
const name = data.name ?? f.replace(/\.md$/, "");
|
|
450
|
+
const title = data.title ?? name;
|
|
451
|
+
const products = Array.isArray(data.products) ? data.products.join(", ") : "unknown";
|
|
452
|
+
const trigger = data.trigger ?? "";
|
|
453
|
+
lines.push(`- ${name}: ${title}`);
|
|
454
|
+
lines.push(` products: ${products}`);
|
|
455
|
+
if (trigger) lines.push(` trigger: ${trigger}`);
|
|
456
|
+
lines.push("");
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
function handleReadSynergy(args) {
|
|
463
|
+
if (!existsSync(SYNERGIES_DIR)) return "(synergies dir not found)";
|
|
464
|
+
if (!SYNERGY_NAME_RE.test(args.name)) {
|
|
465
|
+
return `(synergy not found: ${args.name})`;
|
|
466
|
+
}
|
|
467
|
+
const files = readdirSync(SYNERGIES_DIR).filter((f) => f.endsWith(".md"));
|
|
468
|
+
const targetFile = files.find((f) => basename(f, ".md") === args.name);
|
|
469
|
+
if (targetFile) {
|
|
470
|
+
const raw = readFileSync(join(SYNERGIES_DIR, targetFile), "utf-8");
|
|
471
|
+
return raw;
|
|
472
|
+
}
|
|
473
|
+
for (const f of files) {
|
|
474
|
+
try {
|
|
475
|
+
const raw = readFileSync(join(SYNERGIES_DIR, f), "utf-8");
|
|
476
|
+
const { data } = matter(raw);
|
|
477
|
+
if (data.name === args.name) {
|
|
478
|
+
return raw;
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return `(synergy not found: ${args.name})`;
|
|
484
|
+
}
|
|
485
|
+
function shutdownMcp() {
|
|
486
|
+
try {
|
|
487
|
+
db.close();
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
process.exit(0);
|
|
491
|
+
}
|
|
492
|
+
server.onclose = shutdownMcp;
|
|
493
|
+
process.on("SIGINT", shutdownMcp);
|
|
494
|
+
process.on("SIGTERM", shutdownMcp);
|
|
495
|
+
var transport = new StdioServerTransport();
|
|
496
|
+
await server.connect(transport);
|
|
497
|
+
console.error(`claude-synergy-mcp ready (db: ${DB_PATH}, vec: ${hasChunks ? "on" : "off"})`);
|
package/package.json
CHANGED
|
@@ -1,14 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcptoolshop/claude-synergy",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local mirror of Anthropic product changelogs + curated cross-product synergies. So the LLM agent inside the harness knows what the harness can do.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/cli.js",
|
|
8
|
+
"./mcp": "./dist/mcp-server.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"hk": "./dist/cli.js",
|
|
12
|
+
"claude-synergy-mcp": "./dist/mcp-server.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/cli.ts src/mcp-server.ts --format esm --target node22 --shims --clean --external playwright",
|
|
16
|
+
"dev": "tsx src/cli.ts",
|
|
17
|
+
"mcp": "tsx src/mcp-server.ts",
|
|
18
|
+
"init": "tsx src/cli.ts init",
|
|
19
|
+
"ingest": "tsx src/cli.ts ingest",
|
|
20
|
+
"query": "tsx src/cli.ts query",
|
|
21
|
+
"latest": "tsx src/cli.ts latest",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"test:smoke": "cross-env RUN_SMOKE=1 vitest run test/smoke",
|
|
26
|
+
"prepublishOnly": "tsup src/cli.ts src/mcp-server.ts --format esm --target node22 --shims --clean --external playwright"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
30
|
+
"better-sqlite3": "^11.5.0",
|
|
31
|
+
"cheerio": "^1.0.0",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"fast-xml-parser": "^4.5.0",
|
|
34
|
+
"gray-matter": "^4.0.3",
|
|
35
|
+
"sqlite-vec": "^0.1.9",
|
|
36
|
+
"turndown": "^7.2.0",
|
|
37
|
+
"yaml": "^2.6.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
41
|
+
"@types/node": "^22.10.0",
|
|
42
|
+
"@types/turndown": "^5.0.5",
|
|
43
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
44
|
+
"cross-env": "^7.0.3",
|
|
45
|
+
"playwright": "^1.60.0",
|
|
46
|
+
"tsup": "^8.3.5",
|
|
47
|
+
"tsx": "^4.19.2",
|
|
48
|
+
"typescript": "^5.7.2",
|
|
49
|
+
"vitest": "^2.1.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.0.0"
|
|
53
|
+
},
|
|
54
|
+
"pnpm": {
|
|
55
|
+
"onlyBuiltDependencies": [
|
|
56
|
+
"better-sqlite3",
|
|
57
|
+
"esbuild"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"schema.sql",
|
|
63
|
+
"schema-vec.sql",
|
|
64
|
+
"synergies",
|
|
65
|
+
"products.yaml",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"README.md",
|
|
68
|
+
"CHANGELOG.md",
|
|
69
|
+
"CONTRIBUTING.md"
|
|
70
|
+
],
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"keywords": [
|
|
75
|
+
"claude",
|
|
76
|
+
"anthropic",
|
|
77
|
+
"changelog",
|
|
78
|
+
"synergy",
|
|
79
|
+
"mcp",
|
|
80
|
+
"agent-sdk"
|
|
81
|
+
],
|
|
5
82
|
"license": "MIT",
|
|
6
83
|
"author": "mcp-tool-shop <64996768+mcp-tool-shop@users.noreply.github.com>",
|
|
7
84
|
"repository": {
|
|
8
85
|
"type": "git",
|
|
9
|
-
"url": "
|
|
10
|
-
}
|
|
11
|
-
"files": [
|
|
12
|
-
"README.md"
|
|
13
|
-
]
|
|
86
|
+
"url": "https://github.com/mcp-tool-shop-org/claude-synergy"
|
|
87
|
+
}
|
|
14
88
|
}
|