@lorrylurui/code-intelligence-mcp 1.0.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.
@@ -0,0 +1,219 @@
1
+ import { env } from '../config/env.js';
2
+ import { getMySqlPool } from '../db/mysql.js';
3
+ import { createEmbeddingClient } from '../services/embeddingClient.js';
4
+ import { cosineSimilarity } from '../services/vectorMath.js';
5
+ const inMemorySymbols = [
6
+ {
7
+ id: 1,
8
+ name: 'FormInput',
9
+ type: 'component',
10
+ category: 'form',
11
+ path: 'src/components/FormInput.tsx',
12
+ description: 'A reusable form input with validation',
13
+ content: null,
14
+ meta: { props: ['value', 'onChange', 'error'], hooks: ['useForm'] },
15
+ usageCount: 18,
16
+ createdAt: new Date().toISOString(),
17
+ },
18
+ {
19
+ id: 2,
20
+ name: 'formatDate',
21
+ type: 'util',
22
+ category: 'date',
23
+ path: 'src/utils/date.ts',
24
+ description: 'Format date to YYYY-MM-DD',
25
+ content: null,
26
+ meta: { params: ['input'], returnType: 'string' },
27
+ usageCount: 40,
28
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(),
29
+ },
30
+ ];
31
+ function parseEmbedding(raw) {
32
+ if (raw == null)
33
+ return null;
34
+ if (Array.isArray(raw)) {
35
+ const nums = raw
36
+ .map((x) => Number(x))
37
+ .filter((n) => Number.isFinite(n));
38
+ return nums.length === raw.length ? nums : null;
39
+ }
40
+ if (typeof raw === 'string') {
41
+ try {
42
+ const j = JSON.parse(raw);
43
+ if (!Array.isArray(j))
44
+ return null;
45
+ const nums = j
46
+ .map((x) => Number(x))
47
+ .filter((n) => Number.isFinite(n));
48
+ return nums.length === j.length ? nums : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function mapRow(row, opts) {
57
+ const base = {
58
+ id: row.id,
59
+ name: row.name,
60
+ type: row.type,
61
+ category: row.category,
62
+ path: row.path,
63
+ description: row.description,
64
+ content: row.content,
65
+ meta: row.meta ? JSON.parse(row.meta) : null,
66
+ usageCount: row.usage_count,
67
+ createdAt: row.created_at ?? null,
68
+ };
69
+ if (opts?.includeEmbedding) {
70
+ base.embedding = parseEmbedding(row.embedding);
71
+ }
72
+ return base;
73
+ }
74
+ function getMetaArray(meta, key) {
75
+ if (!meta)
76
+ return [];
77
+ const value = meta[key];
78
+ if (!Array.isArray(value))
79
+ return [];
80
+ return value.filter((v) => typeof v === 'string');
81
+ }
82
+ export class SymbolRepository {
83
+ pool;
84
+ constructor() {
85
+ this.pool = getMySqlPool();
86
+ }
87
+ async search(query, type) {
88
+ if (!this.pool) {
89
+ const q = query.toLowerCase();
90
+ return inMemorySymbols.filter((s) => {
91
+ const typeOk = type ? s.type === type : true;
92
+ return (typeOk &&
93
+ (s.name.toLowerCase().includes(q) ||
94
+ (s.description ?? '').toLowerCase().includes(q)));
95
+ });
96
+ }
97
+ const params = [`%${query}%`];
98
+ let sql = `
99
+ SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
100
+ FROM symbols
101
+ WHERE (name LIKE ? OR description LIKE ?)
102
+ `;
103
+ params.push(`%${query}%`);
104
+ if (type) {
105
+ sql += ' AND type = ?';
106
+ params.push(type);
107
+ }
108
+ sql += ' ORDER BY usage_count DESC LIMIT 20';
109
+ const [rows] = await this.pool.query(sql, params);
110
+ return rows.map((r) => mapRow(r));
111
+ }
112
+ /**
113
+ * Phase 5:对自然语言查询做向量检索,返回代码块与余弦相似度(已去掉 embedding 列便于 JSON 输出)。
114
+ */
115
+ async searchSemanticHits(query, opts) {
116
+ if (!env.embeddingServiceUrl) {
117
+ throw new Error('语义检索需配置 EMBEDDING_SERVICE_URL 并启动嵌入服务');
118
+ }
119
+ if (!this.pool) {
120
+ return [];
121
+ }
122
+ const candidateLimit = opts?.candidateLimit ?? 3000;
123
+ const limit = opts?.limit ?? 20;
124
+ const type = opts?.type;
125
+ const client = createEmbeddingClient(env.embeddingServiceUrl);
126
+ const [queryVec] = await client.embed([query.trim()]);
127
+ if (!queryVec?.length) {
128
+ throw new Error('查询向量为空');
129
+ }
130
+ let sql = `
131
+ SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at, embedding
132
+ FROM symbols
133
+ WHERE embedding IS NOT NULL
134
+ `;
135
+ const params = [];
136
+ if (type) {
137
+ sql += ' AND type = ?';
138
+ params.push(type);
139
+ }
140
+ sql += ' ORDER BY usage_count DESC LIMIT ?';
141
+ params.push(candidateLimit);
142
+ const [rows] = await this.pool.query(sql, params);
143
+ const withVec = rows
144
+ .map((r) => mapRow(r, { includeEmbedding: true }))
145
+ .filter((s) => s.embedding && s.embedding.length === queryVec.length);
146
+ return withVec
147
+ .map((s) => {
148
+ const sim = cosineSimilarity(queryVec, s.embedding);
149
+ const { embedding: _, ...rest } = s;
150
+ return { symbol: rest, similarity: sim };
151
+ })
152
+ .sort((a, b) => b.similarity - a.similarity)
153
+ .slice(0, limit);
154
+ }
155
+ async getByName(name) {
156
+ if (!this.pool) {
157
+ return (inMemorySymbols.find((s) => s.name.toLowerCase() === name.toLowerCase()) ?? null);
158
+ }
159
+ const [rows] = await this.pool.query(`
160
+ SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
161
+ FROM symbols
162
+ WHERE name = ?
163
+ LIMIT 1
164
+ `, [name]);
165
+ if (rows.length === 0) {
166
+ return null;
167
+ }
168
+ return mapRow(rows[0]);
169
+ }
170
+ async searchByStructure(fields, opts) {
171
+ const normalized = fields.map((f) => f.trim()).filter(Boolean);
172
+ if (normalized.length === 0)
173
+ return [];
174
+ const type = opts?.type;
175
+ const category = opts?.category?.trim();
176
+ const limit = opts?.limit ?? 20;
177
+ const matchesAll = (symbol) => {
178
+ const typeOk = type ? symbol.type === type : true;
179
+ const categoryOk = category
180
+ ? (symbol.category ?? '')
181
+ .toLowerCase()
182
+ .includes(category.toLowerCase())
183
+ : true;
184
+ if (!typeOk || !categoryOk)
185
+ return false;
186
+ const propPool = [
187
+ ...getMetaArray(symbol.meta, 'props'),
188
+ ...getMetaArray(symbol.meta, 'params'),
189
+ ...getMetaArray(symbol.meta, 'properties'),
190
+ ...getMetaArray(symbol.meta, 'hooks'),
191
+ ].map((v) => v.toLowerCase());
192
+ return normalized.every((field) => propPool.includes(field.toLowerCase()));
193
+ };
194
+ if (!this.pool) {
195
+ return inMemorySymbols.filter(matchesAll).slice(0, limit);
196
+ }
197
+ const params = [];
198
+ let sql = `
199
+ SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
200
+ FROM symbols
201
+ WHERE 1 = 1
202
+ `;
203
+ if (type) {
204
+ sql += ' AND type = ?';
205
+ params.push(type);
206
+ }
207
+ if (category) {
208
+ sql += ' AND category LIKE ?';
209
+ params.push(`%${category}%`);
210
+ }
211
+ sql += ' ORDER BY usage_count DESC LIMIT ?';
212
+ params.push(Math.max(limit * 5, 50));
213
+ const [rows] = await this.pool.query(sql, params);
214
+ return rows
215
+ .map((r) => mapRow(r))
216
+ .filter(matchesAll)
217
+ .slice(0, limit);
218
+ }
219
+ }
@@ -0,0 +1,29 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerRecommendComponentPrompt } from "../prompts/recommendComponentPrompt.js";
3
+ import { registerReusableCodeAdvisorPrompt } from "../prompts/reusableCodeAdvisorPrompt.js";
4
+ import { SymbolRepository } from "../repositories/symbolRepository.js";
5
+ import { createSearchSymbolsTool } from "../tools/searchSymbols.js";
6
+ import { createGetSymbolDetailTool } from "../tools/getSymbolDetail.js";
7
+ import { createRecommendComponentTool } from "../tools/recommendComponent.js";
8
+ import { createReindexTool } from "../tools/reindex.js";
9
+ import { createSearchByStructureTool } from "../tools/searchByStructure.js";
10
+ export function createServer() {
11
+ const server = new McpServer({
12
+ name: "code-intelligence-mcp",
13
+ version: "0.1.0"
14
+ });
15
+ const repository = new SymbolRepository();
16
+ const searchTool = createSearchSymbolsTool(repository);
17
+ server.tool(searchTool.name, searchTool.description, searchTool.inputSchema, searchTool.handler);
18
+ const detailTool = createGetSymbolDetailTool(repository);
19
+ server.tool(detailTool.name, detailTool.description, detailTool.inputSchema, detailTool.handler);
20
+ const structureTool = createSearchByStructureTool(repository);
21
+ server.tool(structureTool.name, structureTool.description, structureTool.inputSchema, structureTool.handler);
22
+ const recommendTool = createRecommendComponentTool(repository);
23
+ server.tool(recommendTool.name, recommendTool.description, recommendTool.inputSchema, recommendTool.handler);
24
+ const reindexTool = createReindexTool();
25
+ server.tool(reindexTool.name, reindexTool.description, reindexTool.inputSchema, reindexTool.handler);
26
+ registerRecommendComponentPrompt(server);
27
+ registerReusableCodeAdvisorPrompt(server);
28
+ return server;
29
+ }
@@ -0,0 +1,37 @@
1
+ export function createEmbeddingClient(baseUrl) {
2
+ const root = baseUrl.replace(/\/$/, "");
3
+ return {
4
+ async embed(texts) {
5
+ if (texts.length === 0)
6
+ return [];
7
+ const res = await fetch(`${root}/embed`, {
8
+ method: "POST",
9
+ headers: { "Content-Type": "application/json" },
10
+ body: JSON.stringify({ texts }),
11
+ signal: AbortSignal.timeout(180_000)
12
+ });
13
+ if (!res.ok) {
14
+ const t = await res.text().catch(() => "");
15
+ throw new Error(`embedding service HTTP ${res.status}${t ? `: ${t.slice(0, 200)}` : ""}`);
16
+ }
17
+ const data = (await res.json());
18
+ if (!data.embeddings || !Array.isArray(data.embeddings)) {
19
+ throw new Error("embedding service returned invalid JSON (missing embeddings)");
20
+ }
21
+ return data.embeddings;
22
+ }
23
+ };
24
+ }
25
+ /** Chunked embed to avoid huge request bodies. */
26
+ export async function embedAll(client, texts, chunkSize = 48) {
27
+ const out = [];
28
+ for (let i = 0; i < texts.length; i += chunkSize) {
29
+ const chunk = texts.slice(i, i + chunkSize);
30
+ const vecs = await client.embed(chunk);
31
+ if (vecs.length !== chunk.length) {
32
+ throw new Error(`embedding service returned ${vecs.length} vectors for ${chunk.length} texts`);
33
+ }
34
+ out.push(...vecs);
35
+ }
36
+ return out;
37
+ }
@@ -0,0 +1,161 @@
1
+ function clamp01(value) {
2
+ if (value < 0)
3
+ return 0;
4
+ if (value > 1)
5
+ return 1;
6
+ return value;
7
+ }
8
+ function textMatchScore(query, symbol) {
9
+ const q = query.trim().toLowerCase();
10
+ if (!q)
11
+ return { score: 0, matchedBy: "weak" };
12
+ const name = symbol.name.toLowerCase();
13
+ const description = (symbol.description ?? "").toLowerCase();
14
+ if (name === q)
15
+ return { score: 1, matchedBy: "exact_name" };
16
+ if (name.includes(q))
17
+ return { score: 0.85, matchedBy: "name_contains" };
18
+ if (description.includes(q))
19
+ return { score: 0.65, matchedBy: "description_contains" };
20
+ return { score: 0.2, matchedBy: "weak" };
21
+ }
22
+ function usageScore(usageCount) {
23
+ // log scale to avoid very large usage monopolizing ranking.
24
+ return clamp01(Math.log10(usageCount + 1) / 3);
25
+ }
26
+ function recencyScore(createdAt) {
27
+ if (!createdAt)
28
+ return 0.4;
29
+ const ts = new Date(createdAt).getTime();
30
+ if (Number.isNaN(ts))
31
+ return 0.4;
32
+ const days = (Date.now() - ts) / (1000 * 60 * 60 * 24);
33
+ if (days <= 7)
34
+ return 1;
35
+ if (days <= 30)
36
+ return 0.8;
37
+ if (days <= 90)
38
+ return 0.6;
39
+ if (days <= 180)
40
+ return 0.4;
41
+ return 0.25;
42
+ }
43
+ function daysSinceCreated(createdAt) {
44
+ if (!createdAt)
45
+ return null;
46
+ const ts = new Date(createdAt).getTime();
47
+ if (Number.isNaN(ts))
48
+ return null;
49
+ return Math.floor((Date.now() - ts) / (1000 * 60 * 60 * 24));
50
+ }
51
+ function commonPathScore(path) {
52
+ const lower = path.toLowerCase();
53
+ return lower.includes("/common/") || lower.includes("/shared/") ? 1 : 0.35;
54
+ }
55
+ const RANK_WEIGHTS = {
56
+ textMatch: 0.5,
57
+ usage: 0.3,
58
+ recency: 0.1,
59
+ commonPath: 0.1
60
+ };
61
+ /**
62
+ * Phase 5:以向量余弦相似度作为主文本维度,再叠加 usage / recency / common(与 `rankSymbols` 同权重)。
63
+ */
64
+ export function rankSemanticHits(hits) {
65
+ return hits
66
+ .map(({ symbol, similarity }) => {
67
+ const textScore = clamp01(similarity);
68
+ const usage = usageScore(symbol.usageCount);
69
+ const recency = recencyScore(symbol.createdAt);
70
+ const common = commonPathScore(symbol.path);
71
+ const score = textScore * RANK_WEIGHTS.textMatch +
72
+ usage * RANK_WEIGHTS.usage +
73
+ recency * RANK_WEIGHTS.recency +
74
+ common * RANK_WEIGHTS.commonPath;
75
+ const reasonParts = [];
76
+ if (textScore >= 0.55)
77
+ reasonParts.push("语义相似度高");
78
+ else if (textScore >= 0.4)
79
+ reasonParts.push("语义相关");
80
+ if (usage >= 0.6)
81
+ reasonParts.push("使用频率高");
82
+ if (common >= 1)
83
+ reasonParts.push("位于 shared/common 路径");
84
+ if (reasonParts.length === 0)
85
+ reasonParts.push("综合相关性较好");
86
+ return {
87
+ symbol,
88
+ score: Number(score.toFixed(3)),
89
+ reason: {
90
+ textMatch: {
91
+ score: Number(textScore.toFixed(3)),
92
+ matchedBy: "semantic"
93
+ },
94
+ usage: {
95
+ score: Number(usage.toFixed(3)),
96
+ usageCount: symbol.usageCount
97
+ },
98
+ recency: {
99
+ score: Number(recency.toFixed(3)),
100
+ daysSinceCreated: daysSinceCreated(symbol.createdAt)
101
+ },
102
+ commonPath: {
103
+ score: Number(common.toFixed(3)),
104
+ isCommonPath: common >= 1
105
+ },
106
+ weights: RANK_WEIGHTS,
107
+ summary: reasonParts.join(" + ")
108
+ }
109
+ };
110
+ })
111
+ .sort((a, b) => b.score - a.score);
112
+ }
113
+ export function rankSymbols(query, symbols) {
114
+ return symbols
115
+ .map((symbol) => {
116
+ const text = textMatchScore(query, symbol);
117
+ const usage = usageScore(symbol.usageCount);
118
+ const recency = recencyScore(symbol.createdAt);
119
+ const common = commonPathScore(symbol.path);
120
+ const score = text.score * RANK_WEIGHTS.textMatch +
121
+ usage * RANK_WEIGHTS.usage +
122
+ recency * RANK_WEIGHTS.recency +
123
+ common * RANK_WEIGHTS.commonPath;
124
+ const reasonParts = [];
125
+ if (text.score >= 0.85)
126
+ reasonParts.push("文本匹配度高");
127
+ else if (text.score >= 0.65)
128
+ reasonParts.push("描述命中");
129
+ if (usage >= 0.6)
130
+ reasonParts.push("使用频率高");
131
+ if (common >= 1)
132
+ reasonParts.push("位于 shared/common 路径");
133
+ if (reasonParts.length === 0)
134
+ reasonParts.push("综合相关性较好");
135
+ return {
136
+ symbol,
137
+ score: Number(score.toFixed(3)),
138
+ reason: {
139
+ textMatch: {
140
+ score: Number(text.score.toFixed(3)),
141
+ matchedBy: text.matchedBy
142
+ },
143
+ usage: {
144
+ score: Number(usage.toFixed(3)),
145
+ usageCount: symbol.usageCount
146
+ },
147
+ recency: {
148
+ score: Number(recency.toFixed(3)),
149
+ daysSinceCreated: daysSinceCreated(symbol.createdAt)
150
+ },
151
+ commonPath: {
152
+ score: Number(common.toFixed(3)),
153
+ isCommonPath: common >= 1
154
+ },
155
+ weights: RANK_WEIGHTS,
156
+ summary: reasonParts.join(" + ")
157
+ }
158
+ };
159
+ })
160
+ .sort((a, b) => b.score - a.score);
161
+ }
@@ -0,0 +1,45 @@
1
+ import { resolve } from "node:path";
2
+ import { env, validateEnv } from "../config/env.js";
3
+ import { getMySqlPool } from "../db/mysql.js";
4
+ import { indexedRowToEmbedText } from "../indexer/embedText.js";
5
+ import { indexProject } from "../indexer/indexProject.js";
6
+ import { upsertSymbols } from "../indexer/persistSymbols.js";
7
+ import { createEmbeddingClient, embedAll } from "../services/embeddingClient.js";
8
+ export async function runReindex(options = {}) {
9
+ validateEnv();
10
+ const pool = getMySqlPool();
11
+ if (!pool || !env.mysqlEnabled) {
12
+ throw new Error("执行 reindex 前必须开启 MYSQL_ENABLED=true。");
13
+ }
14
+ await pool.query("SELECT 1");
15
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
16
+ const rows = await indexProject({
17
+ projectRoot,
18
+ globPatterns: options.globPatterns,
19
+ ignore: options.ignore
20
+ });
21
+ let embeddingsComputed = false;
22
+ let embeddingPayload;
23
+ if (!options.dryRun && rows.length > 0 && env.embeddingServiceUrl) {
24
+ try {
25
+ const client = createEmbeddingClient(env.embeddingServiceUrl);
26
+ const texts = rows.map(indexedRowToEmbedText);
27
+ const vecs = await embedAll(client, texts);
28
+ embeddingPayload = vecs;
29
+ embeddingsComputed = true;
30
+ }
31
+ catch (err) {
32
+ console.error("[reindex] embedding skipped (service error):", err);
33
+ embeddingPayload = rows.map(() => null);
34
+ }
35
+ }
36
+ if (!options.dryRun) {
37
+ await upsertSymbols(pool, rows, embeddingPayload);
38
+ }
39
+ return {
40
+ projectRoot,
41
+ extractedCount: rows.length,
42
+ upserted: !options.dryRun,
43
+ embeddingsComputed
44
+ };
45
+ }
@@ -0,0 +1,17 @@
1
+ /** Cosine similarity for equal-length numeric vectors. */
2
+ export function cosineSimilarity(a, b) {
3
+ if (a.length === 0 || b.length !== a.length)
4
+ return 0;
5
+ let dot = 0;
6
+ let na = 0;
7
+ let nb = 0;
8
+ for (let i = 0; i < a.length; i++) {
9
+ dot += a[i] * b[i];
10
+ na += a[i] * a[i];
11
+ nb += b[i] * b[i];
12
+ }
13
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
14
+ if (denom === 0)
15
+ return 0;
16
+ return dot / denom;
17
+ }
@@ -0,0 +1,24 @@
1
+ function keywordScore(query, symbol) {
2
+ const q = query.trim().toLowerCase();
3
+ if (!q)
4
+ return 0.5;
5
+ if (symbol.name.toLowerCase().includes(q))
6
+ return 1;
7
+ if ((symbol.description ?? "").toLowerCase().includes(q))
8
+ return 0.8;
9
+ return 0.4;
10
+ }
11
+ export function rankSymbols(query, symbols) {
12
+ return symbols
13
+ .map((item) => {
14
+ const text = keywordScore(query, item);
15
+ const usage = Math.min(item.usageCount, 100) / 100;
16
+ const score = text * 0.7 + usage * 0.3;
17
+ return {
18
+ ...item,
19
+ score: Number(score.toFixed(3)),
20
+ reason: "text match + usage weight"
21
+ };
22
+ })
23
+ .sort((a, b) => b.score - a.score);
24
+ }
@@ -0,0 +1,39 @@
1
+ import { rankSymbols } from "../services/ranking.js";
2
+ export async function recommendComponent(query, repository, options = {}) {
3
+ const requestedProps = options.props?.map((p) => p.trim()).filter(Boolean) ?? [];
4
+ const limit = options.limit ?? 3;
5
+ // Step 1: keyword candidate search
6
+ const keywordCandidates = await repository.search(query, "component");
7
+ // Step 2: optional structure filter
8
+ const structureCandidates = requestedProps.length > 0
9
+ ? await repository.searchByStructure(requestedProps, {
10
+ type: "component",
11
+ limit: Math.max(limit * 3, 20)
12
+ })
13
+ : [];
14
+ const byName = new Map();
15
+ for (const row of keywordCandidates)
16
+ byName.set(row.name, row);
17
+ for (const row of structureCandidates)
18
+ byName.set(row.name, row);
19
+ // Step 3: ranking
20
+ const ranked = rankSymbols(query, [...byName.values()]).slice(0, Math.max(limit * 3, 10));
21
+ // Step 4: detail enrichment
22
+ const enriched = await Promise.all(ranked.map(async (item) => {
23
+ const detail = await repository.getByName(item.symbol.name);
24
+ return {
25
+ name: item.symbol.name,
26
+ path: item.symbol.path,
27
+ score: item.score,
28
+ reason: item.reason.summary,
29
+ reasonDetail: item.reason,
30
+ detail
31
+ };
32
+ }));
33
+ // Step 5: return top-N with reasons
34
+ return {
35
+ query,
36
+ requestedProps,
37
+ results: enriched.slice(0, limit)
38
+ };
39
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export const getSymbolDetailInput = z.object({
3
+ name: z.string().min(1),
4
+ });
5
+ export function createGetSymbolDetailTool(repository) {
6
+ return {
7
+ name: 'get_symbol_detail',
8
+ description: '按名称获取单个代码块的完整详情。',
9
+ inputSchema: getSymbolDetailInput.shape,
10
+ handler: async (input) => {
11
+ const symbol = await repository.getByName(input.name);
12
+ if (!symbol) {
13
+ return {
14
+ content: [
15
+ {
16
+ type: 'text',
17
+ text: `未找到代码块:${input.name}`,
18
+ },
19
+ ],
20
+ };
21
+ }
22
+ return {
23
+ content: [
24
+ {
25
+ type: 'text',
26
+ text: JSON.stringify(symbol, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { recommendComponent } from "../skills/recommendComponent.js";
3
+ export const recommendComponentInput = z.object({
4
+ query: z.string().min(1),
5
+ props: z.array(z.string().min(1)).optional(),
6
+ limit: z.number().int().min(1).max(20).optional().default(3)
7
+ });
8
+ export function createRecommendComponentTool(repository) {
9
+ return {
10
+ name: "recommend_component",
11
+ description: "基于关键词检索 + 可选结构过滤 + 排序 + 详情补全,推荐最合适的可复用组件。",
12
+ inputSchema: recommendComponentInput.shape,
13
+ handler: async (input) => {
14
+ const result = await recommendComponent(input.query, repository, {
15
+ props: input.props,
16
+ limit: input.limit
17
+ });
18
+ return {
19
+ content: [
20
+ {
21
+ type: "text",
22
+ text: JSON.stringify(result, null, 2)
23
+ }
24
+ ]
25
+ };
26
+ }
27
+ };
28
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { runReindex } from '../services/reindex.js';
3
+ export const reindexInput = z.object({
4
+ projectRoot: z.string().optional(),
5
+ globPatterns: z.array(z.string().min(1)).optional(),
6
+ ignore: z.array(z.string().min(1)).optional(),
7
+ dryRun: z.boolean().optional().default(false),
8
+ });
9
+ export function createReindexTool() {
10
+ return {
11
+ name: 'reindex',
12
+ description: '重建源码代码块索引并写入 MySQL;设置 dryRun=true 时仅预览抽取数量,不落库、不调用嵌入服务。若配置 EMBEDDING_SERVICE_URL,非 dryRun 时会写入向量列。',
13
+ inputSchema: reindexInput.shape,
14
+ handler: async (input) => {
15
+ const startedAt = Date.now();
16
+ const result = await runReindex({
17
+ projectRoot: input.projectRoot,
18
+ globPatterns: input.globPatterns,
19
+ ignore: input.ignore,
20
+ dryRun: input.dryRun,
21
+ });
22
+ const elapsedMs = Date.now() - startedAt;
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: JSON.stringify({
28
+ ok: true,
29
+ ...result,
30
+ elapsedMs,
31
+ }, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ },
36
+ };
37
+ }