bluera-knowledge 0.9.26 → 0.9.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/commit.md +4 -7
- package/.claude/hooks/post-edit-check.sh +21 -24
- package/.claude/skills/atomic-commits/SKILL.md +6 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.env.example +4 -0
- package/.husky/pre-push +12 -2
- package/.versionrc.json +0 -4
- package/BUGS-FOUND.md +71 -0
- package/CHANGELOG.md +76 -0
- package/README.md +55 -20
- package/bun.lock +35 -1
- package/commands/crawl.md +2 -0
- package/dist/{chunk-BICFAWMN.js → chunk-2SJHNRXD.js} +73 -8
- package/dist/chunk-2SJHNRXD.js.map +1 -0
- package/dist/{chunk-J7J6LXOJ.js → chunk-OGEY66FZ.js} +106 -41
- package/dist/chunk-OGEY66FZ.js.map +1 -0
- package/dist/{chunk-5QMHZUC4.js → chunk-RWSXP3PQ.js} +482 -106
- package/dist/chunk-RWSXP3PQ.js.map +1 -0
- package/dist/index.js +73 -28
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/eslint.config.js +1 -1
- package/package.json +3 -1
- package/src/analysis/ast-parser.test.ts +46 -0
- package/src/cli/commands/crawl.test.ts +99 -12
- package/src/cli/commands/crawl.ts +76 -24
- package/src/cli/commands/store.test.ts +68 -1
- package/src/cli/commands/store.ts +9 -3
- package/src/crawl/article-converter.ts +36 -1
- package/src/crawl/bridge.ts +18 -7
- package/src/crawl/intelligent-crawler.ts +45 -4
- package/src/db/embeddings.test.ts +16 -0
- package/src/db/lance.test.ts +31 -0
- package/src/db/lance.ts +8 -0
- package/src/logging/index.ts +29 -0
- package/src/logging/logger.test.ts +75 -0
- package/src/logging/logger.ts +147 -0
- package/src/logging/payload.test.ts +152 -0
- package/src/logging/payload.ts +121 -0
- package/src/mcp/handlers/search.handler.test.ts +28 -9
- package/src/mcp/handlers/search.handler.ts +69 -29
- package/src/mcp/handlers/store.handler.test.ts +1 -0
- package/src/mcp/server.ts +44 -16
- package/src/services/chunking.service.ts +23 -0
- package/src/services/index.service.test.ts +921 -1
- package/src/services/index.service.ts +76 -1
- package/src/services/index.ts +20 -2
- package/src/services/search.service.test.ts +573 -21
- package/src/services/search.service.ts +257 -105
- package/src/services/services.test.ts +2 -2
- package/src/services/snippet.service.ts +28 -3
- package/src/services/store.service.test.ts +28 -0
- package/src/services/store.service.ts +4 -0
- package/src/services/token.service.test.ts +45 -0
- package/src/services/token.service.ts +33 -0
- package/src/types/result.test.ts +10 -0
- package/tests/integration/cli-consistency.test.ts +1 -4
- package/vitest.config.ts +4 -0
- package/dist/chunk-5QMHZUC4.js.map +0 -1
- package/dist/chunk-BICFAWMN.js.map +0 -1
- package/dist/chunk-J7J6LXOJ.js.map +0 -1
- package/scripts/readme-version-updater.cjs +0 -18
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large payload handling utilities for logging
|
|
3
|
+
*
|
|
4
|
+
* Handles large content (raw HTML, MCP responses) by:
|
|
5
|
+
* - Truncating to preview in log entries
|
|
6
|
+
* - Optionally dumping full content to separate files at trace level
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { getLogDirectory, isLevelEnabled } from './logger.js';
|
|
13
|
+
|
|
14
|
+
/** Maximum characters for log preview */
|
|
15
|
+
const MAX_PREVIEW_LENGTH = 500;
|
|
16
|
+
|
|
17
|
+
/** Minimum size to trigger payload dump (10KB) */
|
|
18
|
+
const PAYLOAD_DUMP_THRESHOLD = 10_000;
|
|
19
|
+
|
|
20
|
+
/** Summary of a large payload for logging */
|
|
21
|
+
export interface PayloadSummary {
|
|
22
|
+
/** Truncated preview of content */
|
|
23
|
+
preview: string;
|
|
24
|
+
/** Size in bytes */
|
|
25
|
+
sizeBytes: number;
|
|
26
|
+
/** Short hash for identification */
|
|
27
|
+
hash: string;
|
|
28
|
+
/** Filename if full content was dumped (trace level only) */
|
|
29
|
+
payloadFile?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Get the payload dump directory */
|
|
33
|
+
function getPayloadDir(): string {
|
|
34
|
+
const dir = join(getLogDirectory(), 'payload');
|
|
35
|
+
if (!existsSync(dir)) {
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Generate a safe filename from an identifier */
|
|
42
|
+
function safeFilename(identifier: string): string {
|
|
43
|
+
return identifier
|
|
44
|
+
.replace(/[^a-zA-Z0-9-]/g, '_')
|
|
45
|
+
.substring(0, 50);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Summarize a large payload for logging
|
|
50
|
+
*
|
|
51
|
+
* Creates a summary with:
|
|
52
|
+
* - Truncated preview (first 500 chars)
|
|
53
|
+
* - Size in bytes
|
|
54
|
+
* - Short MD5 hash for identification
|
|
55
|
+
* - Optional full dump to file at trace level
|
|
56
|
+
*
|
|
57
|
+
* @param content - The full content to summarize
|
|
58
|
+
* @param type - Type identifier (e.g., 'raw-html', 'mcp-response')
|
|
59
|
+
* @param identifier - Unique identifier (e.g., URL, query)
|
|
60
|
+
* @param dumpFull - Whether to dump full content to file (default: trace level check)
|
|
61
|
+
* @returns PayloadSummary for inclusion in log entry
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* logger.info({
|
|
65
|
+
* url,
|
|
66
|
+
* ...summarizePayload(html, 'raw-html', url),
|
|
67
|
+
* }, 'Fetched HTML');
|
|
68
|
+
*/
|
|
69
|
+
export function summarizePayload(
|
|
70
|
+
content: string,
|
|
71
|
+
type: string,
|
|
72
|
+
identifier: string,
|
|
73
|
+
dumpFull: boolean = isLevelEnabled('trace')
|
|
74
|
+
): PayloadSummary {
|
|
75
|
+
const sizeBytes = Buffer.byteLength(content, 'utf8');
|
|
76
|
+
const hash = createHash('md5').update(content).digest('hex').substring(0, 12);
|
|
77
|
+
const preview = truncateForLog(content, MAX_PREVIEW_LENGTH);
|
|
78
|
+
|
|
79
|
+
const baseSummary = { preview, sizeBytes, hash };
|
|
80
|
+
|
|
81
|
+
// Dump full payload to file if enabled and above threshold
|
|
82
|
+
if (dumpFull && sizeBytes > PAYLOAD_DUMP_THRESHOLD) {
|
|
83
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
84
|
+
const safeId = safeFilename(identifier);
|
|
85
|
+
const filename = `${timestamp}-${type}-${safeId}-${hash}.json`;
|
|
86
|
+
const filepath = join(getPayloadDir(), filename);
|
|
87
|
+
|
|
88
|
+
writeFileSync(
|
|
89
|
+
filepath,
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
type,
|
|
94
|
+
identifier,
|
|
95
|
+
sizeBytes,
|
|
96
|
+
content,
|
|
97
|
+
},
|
|
98
|
+
null,
|
|
99
|
+
2
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return { ...baseSummary, payloadFile: filename };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return baseSummary;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Truncate content for logging with ellipsis indicator
|
|
111
|
+
*
|
|
112
|
+
* @param content - Content to truncate
|
|
113
|
+
* @param maxLength - Maximum length (default: 500)
|
|
114
|
+
* @returns Truncated string with '... [truncated]' if needed
|
|
115
|
+
*/
|
|
116
|
+
export function truncateForLog(content: string, maxLength: number = MAX_PREVIEW_LENGTH): string {
|
|
117
|
+
if (content.length <= maxLength) {
|
|
118
|
+
return content;
|
|
119
|
+
}
|
|
120
|
+
return content.substring(0, maxLength) + '... [truncated]';
|
|
121
|
+
}
|
|
@@ -3,6 +3,20 @@ import { handleSearch, handleGetFullContext, resultCache } from './search.handle
|
|
|
3
3
|
import type { HandlerContext } from '../types.js';
|
|
4
4
|
import type { ServiceContainer } from '../../services/index.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Extract JSON from search response that includes a header line.
|
|
8
|
+
* Format: "Search: ... | Results: ... | ~X tokens | Xms\n\n{json}"
|
|
9
|
+
*/
|
|
10
|
+
function parseSearchResponse(text: string): { header: string; json: Record<string, unknown> } {
|
|
11
|
+
const parts = text.split('\n\n');
|
|
12
|
+
const header = parts[0] ?? '';
|
|
13
|
+
const jsonStr = parts.slice(1).join('\n\n');
|
|
14
|
+
return {
|
|
15
|
+
header,
|
|
16
|
+
json: JSON.parse(jsonStr || '{}')
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
describe('Search Handlers', () => {
|
|
7
21
|
let mockContext: HandlerContext;
|
|
8
22
|
let mockServices: ServiceContainer;
|
|
@@ -70,7 +84,10 @@ describe('Search Handlers', () => {
|
|
|
70
84
|
})
|
|
71
85
|
);
|
|
72
86
|
|
|
73
|
-
const response =
|
|
87
|
+
const { header, json: response } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
88
|
+
expect(header).toContain('Search: "test query"');
|
|
89
|
+
expect(header).toContain('Results: 1');
|
|
90
|
+
expect(header).toContain('tokens');
|
|
74
91
|
expect(response.results).toHaveLength(1);
|
|
75
92
|
expect(response.totalResults).toBe(1);
|
|
76
93
|
});
|
|
@@ -129,14 +146,15 @@ describe('Search Handlers', () => {
|
|
|
129
146
|
expect(cached?.id).toBe('doc1');
|
|
130
147
|
});
|
|
131
148
|
|
|
132
|
-
it('should
|
|
149
|
+
it('should show token count in header', async () => {
|
|
133
150
|
const result = await handleSearch(
|
|
134
151
|
{ query: 'test', detail: 'minimal', limit: 10 },
|
|
135
152
|
mockContext
|
|
136
153
|
);
|
|
137
154
|
|
|
138
|
-
const
|
|
139
|
-
|
|
155
|
+
const { header } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
156
|
+
// Header should contain token count (either "~X tokens" or "~X.Xk tokens")
|
|
157
|
+
expect(header).toMatch(/~\d+\.?\d*k? tokens/);
|
|
140
158
|
});
|
|
141
159
|
|
|
142
160
|
it('should add repoRoot for repo stores', async () => {
|
|
@@ -156,7 +174,7 @@ describe('Search Handlers', () => {
|
|
|
156
174
|
mockContext
|
|
157
175
|
);
|
|
158
176
|
|
|
159
|
-
const response =
|
|
177
|
+
const { json: response } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
160
178
|
expect(response.results[0]?.summary.repoRoot).toBe('/repos/test');
|
|
161
179
|
});
|
|
162
180
|
|
|
@@ -166,7 +184,7 @@ describe('Search Handlers', () => {
|
|
|
166
184
|
mockContext
|
|
167
185
|
);
|
|
168
186
|
|
|
169
|
-
const response =
|
|
187
|
+
const { json: response } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
170
188
|
expect(response.results[0]?.summary.repoRoot).toBeUndefined();
|
|
171
189
|
});
|
|
172
190
|
|
|
@@ -176,7 +194,7 @@ describe('Search Handlers', () => {
|
|
|
176
194
|
mockContext
|
|
177
195
|
);
|
|
178
196
|
|
|
179
|
-
const response =
|
|
197
|
+
const { json: response } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
180
198
|
expect(response.results[0]?.summary.storeName).toBe('Test Store');
|
|
181
199
|
});
|
|
182
200
|
|
|
@@ -186,11 +204,12 @@ describe('Search Handlers', () => {
|
|
|
186
204
|
mockContext
|
|
187
205
|
);
|
|
188
206
|
|
|
189
|
-
const response =
|
|
207
|
+
const { header, json: response } = parseSearchResponse(result.content[0]?.text ?? '');
|
|
190
208
|
expect(response).toHaveProperty('totalResults', 1);
|
|
191
|
-
expect(response).toHaveProperty('estimatedTokens');
|
|
192
209
|
expect(response).toHaveProperty('mode', 'hybrid');
|
|
193
210
|
expect(response).toHaveProperty('timeMs', 50);
|
|
211
|
+
// Token count is now in header, not in JSON
|
|
212
|
+
expect(header).toContain('tokens');
|
|
194
213
|
});
|
|
195
214
|
});
|
|
196
215
|
|
|
@@ -4,6 +4,10 @@ import { SearchArgsSchema, GetFullContextArgsSchema } from '../schemas/index.js'
|
|
|
4
4
|
import type { SearchQuery, DocumentId, StoreId } from '../../types/index.js';
|
|
5
5
|
import { LRUCache } from '../cache.js';
|
|
6
6
|
import type { SearchResult } from '../../types/search.js';
|
|
7
|
+
import { createLogger, summarizePayload } from '../../logging/index.js';
|
|
8
|
+
import { estimateTokens, formatTokenCount } from '../../services/token.service.js';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('mcp-search');
|
|
7
11
|
|
|
8
12
|
// Create result cache for get_full_context
|
|
9
13
|
// Uses LRU cache to prevent memory leaks (max 1000 items)
|
|
@@ -22,6 +26,14 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
22
26
|
// Validate arguments with Zod
|
|
23
27
|
const validated = SearchArgsSchema.parse(args);
|
|
24
28
|
|
|
29
|
+
logger.info({
|
|
30
|
+
query: validated.query,
|
|
31
|
+
stores: validated.stores,
|
|
32
|
+
detail: validated.detail,
|
|
33
|
+
limit: validated.limit,
|
|
34
|
+
intent: validated.intent,
|
|
35
|
+
}, 'Search started');
|
|
36
|
+
|
|
25
37
|
const { services } = context;
|
|
26
38
|
|
|
27
39
|
// Get all stores if none specified, resolve store names to IDs
|
|
@@ -63,14 +75,6 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
63
75
|
resultCache.set(result.id, result);
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
// Calculate estimated tokens
|
|
67
|
-
const estimatedTokens = results.results.reduce((sum, r) => {
|
|
68
|
-
let tokens = 100; // Base for summary
|
|
69
|
-
if (r.context) tokens += 200;
|
|
70
|
-
if (r.full) tokens += 800;
|
|
71
|
-
return sum + tokens;
|
|
72
|
-
}, 0);
|
|
73
|
-
|
|
74
78
|
// Add repoRoot to results for cloned repos
|
|
75
79
|
const enhancedResults = await Promise.all(results.results.map(async (r) => {
|
|
76
80
|
const storeId = r.metadata.storeId;
|
|
@@ -89,17 +93,33 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
89
93
|
};
|
|
90
94
|
}));
|
|
91
95
|
|
|
96
|
+
const responseJson = JSON.stringify({
|
|
97
|
+
results: enhancedResults,
|
|
98
|
+
totalResults: results.totalResults,
|
|
99
|
+
mode: results.mode,
|
|
100
|
+
timeMs: results.timeMs
|
|
101
|
+
}, null, 2);
|
|
102
|
+
|
|
103
|
+
// Calculate actual token estimate based on response content
|
|
104
|
+
const responseTokens = estimateTokens(responseJson);
|
|
105
|
+
|
|
106
|
+
// Create visible header with token usage
|
|
107
|
+
const header = `Search: "${validated.query}" | Results: ${String(results.totalResults)} | ${formatTokenCount(responseTokens)} tokens | ${String(results.timeMs)}ms\n\n`;
|
|
108
|
+
|
|
109
|
+
// Log the complete MCP response that will be sent to Claude Code
|
|
110
|
+
logger.info({
|
|
111
|
+
query: validated.query,
|
|
112
|
+
totalResults: results.totalResults,
|
|
113
|
+
responseTokens,
|
|
114
|
+
timeMs: results.timeMs,
|
|
115
|
+
...summarizePayload(responseJson, 'mcp-response', validated.query),
|
|
116
|
+
}, 'Search complete - context sent to Claude Code');
|
|
117
|
+
|
|
92
118
|
return {
|
|
93
119
|
content: [
|
|
94
120
|
{
|
|
95
121
|
type: 'text',
|
|
96
|
-
text:
|
|
97
|
-
results: enhancedResults,
|
|
98
|
-
totalResults: results.totalResults,
|
|
99
|
-
estimatedTokens,
|
|
100
|
-
mode: results.mode,
|
|
101
|
-
timeMs: results.timeMs
|
|
102
|
-
}, null, 2)
|
|
122
|
+
text: header + responseJson
|
|
103
123
|
}
|
|
104
124
|
]
|
|
105
125
|
};
|
|
@@ -118,6 +138,8 @@ export const handleGetFullContext: ToolHandler<GetFullContextArgs> = async (
|
|
|
118
138
|
// Validate arguments with Zod
|
|
119
139
|
const validated = GetFullContextArgsSchema.parse(args);
|
|
120
140
|
|
|
141
|
+
logger.info({ resultId: validated.resultId }, 'Get full context requested');
|
|
142
|
+
|
|
121
143
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
122
144
|
const resultId = validated.resultId as DocumentId;
|
|
123
145
|
|
|
@@ -132,17 +154,26 @@ export const handleGetFullContext: ToolHandler<GetFullContextArgs> = async (
|
|
|
132
154
|
|
|
133
155
|
// If result already has full context, return it
|
|
134
156
|
if (cachedResult.full) {
|
|
157
|
+
const responseJson = JSON.stringify({
|
|
158
|
+
id: cachedResult.id,
|
|
159
|
+
score: cachedResult.score,
|
|
160
|
+
summary: cachedResult.summary,
|
|
161
|
+
context: cachedResult.context,
|
|
162
|
+
full: cachedResult.full
|
|
163
|
+
}, null, 2);
|
|
164
|
+
|
|
165
|
+
logger.info({
|
|
166
|
+
resultId,
|
|
167
|
+
cached: true,
|
|
168
|
+
hasFullContext: true,
|
|
169
|
+
...summarizePayload(responseJson, 'mcp-full-context', resultId),
|
|
170
|
+
}, 'Full context retrieved from cache');
|
|
171
|
+
|
|
135
172
|
return {
|
|
136
173
|
content: [
|
|
137
174
|
{
|
|
138
175
|
type: 'text',
|
|
139
|
-
text:
|
|
140
|
-
id: cachedResult.id,
|
|
141
|
-
score: cachedResult.score,
|
|
142
|
-
summary: cachedResult.summary,
|
|
143
|
-
context: cachedResult.context,
|
|
144
|
-
full: cachedResult.full
|
|
145
|
-
}, null, 2)
|
|
176
|
+
text: responseJson
|
|
146
177
|
}
|
|
147
178
|
]
|
|
148
179
|
};
|
|
@@ -192,17 +223,26 @@ export const handleGetFullContext: ToolHandler<GetFullContextArgs> = async (
|
|
|
192
223
|
// Update cache with full result
|
|
193
224
|
resultCache.set(resultId, fullResult);
|
|
194
225
|
|
|
226
|
+
const responseJson = JSON.stringify({
|
|
227
|
+
id: fullResult.id,
|
|
228
|
+
score: fullResult.score,
|
|
229
|
+
summary: fullResult.summary,
|
|
230
|
+
context: fullResult.context,
|
|
231
|
+
full: fullResult.full
|
|
232
|
+
}, null, 2);
|
|
233
|
+
|
|
234
|
+
logger.info({
|
|
235
|
+
resultId,
|
|
236
|
+
cached: false,
|
|
237
|
+
hasFullContext: true,
|
|
238
|
+
...summarizePayload(responseJson, 'mcp-full-context', resultId),
|
|
239
|
+
}, 'Full context retrieved via re-query');
|
|
240
|
+
|
|
195
241
|
return {
|
|
196
242
|
content: [
|
|
197
243
|
{
|
|
198
244
|
type: 'text',
|
|
199
|
-
text:
|
|
200
|
-
id: fullResult.id,
|
|
201
|
-
score: fullResult.score,
|
|
202
|
-
summary: fullResult.summary,
|
|
203
|
-
context: fullResult.context,
|
|
204
|
-
full: fullResult.full
|
|
205
|
-
}, null, 2)
|
|
245
|
+
text: responseJson
|
|
206
246
|
}
|
|
207
247
|
]
|
|
208
248
|
};
|
package/src/mcp/server.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { tools } from './handlers/index.js';
|
|
|
9
9
|
import { handleExecute } from './handlers/execute.handler.js';
|
|
10
10
|
import { ExecuteArgsSchema } from './schemas/index.js';
|
|
11
11
|
import type { MCPServerOptions } from './types.js';
|
|
12
|
+
import { createLogger } from '../logging/index.js';
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('mcp-server');
|
|
12
15
|
|
|
13
16
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
14
17
|
export function createMCPServer(options: MCPServerOptions): Server {
|
|
@@ -106,6 +109,9 @@ export function createMCPServer(options: MCPServerOptions): Server {
|
|
|
106
109
|
// Handle tool calls
|
|
107
110
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
108
111
|
const { name, arguments: args } = request.params;
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
|
|
114
|
+
logger.info({ tool: name, args: JSON.stringify(args) }, 'Tool invoked');
|
|
109
115
|
|
|
110
116
|
// Create services once (needed by all handlers)
|
|
111
117
|
const services = await createServices(
|
|
@@ -115,34 +121,56 @@ export function createMCPServer(options: MCPServerOptions): Server {
|
|
|
115
121
|
);
|
|
116
122
|
const context = { services, options };
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const validated = ExecuteArgsSchema.parse(args ?? {});
|
|
121
|
-
return handleExecute(validated, context);
|
|
122
|
-
}
|
|
124
|
+
try {
|
|
125
|
+
let result;
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// Handle execute meta-tool
|
|
128
|
+
if (name === 'execute') {
|
|
129
|
+
const validated = ExecuteArgsSchema.parse(args ?? {});
|
|
130
|
+
result = await handleExecute(validated, context);
|
|
131
|
+
} else {
|
|
132
|
+
// Find handler in registry for native tools (search, get_full_context)
|
|
133
|
+
const tool = tools.find(t => t.name === name);
|
|
134
|
+
if (tool === undefined) {
|
|
135
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
136
|
+
}
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
// Validate arguments with Zod
|
|
139
|
+
const validated = tool.schema.parse(args ?? {});
|
|
132
140
|
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
// Execute handler with context
|
|
142
|
+
result = await tool.handler(validated, context);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const durationMs = Date.now() - startTime;
|
|
146
|
+
logger.info({ tool: name, durationMs }, 'Tool completed');
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const durationMs = Date.now() - startTime;
|
|
151
|
+
logger.error({
|
|
152
|
+
tool: name,
|
|
153
|
+
durationMs,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
155
|
+
}, 'Tool execution failed');
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
135
158
|
});
|
|
136
159
|
|
|
137
160
|
return server;
|
|
138
161
|
}
|
|
139
162
|
|
|
140
163
|
export async function runMCPServer(options: MCPServerOptions): Promise<void> {
|
|
164
|
+
logger.info({
|
|
165
|
+
dataDir: options.dataDir,
|
|
166
|
+
projectRoot: options.projectRoot,
|
|
167
|
+
}, 'MCP server starting');
|
|
168
|
+
|
|
141
169
|
const server = createMCPServer(options);
|
|
142
170
|
const transport = new StdioServerTransport();
|
|
143
171
|
await server.connect(transport);
|
|
144
172
|
|
|
145
|
-
|
|
173
|
+
logger.info('MCP server connected to stdio transport');
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
// Run the server only when this file is executed directly (not imported by CLI)
|
|
@@ -156,7 +184,7 @@ if (isMCPServerEntry) {
|
|
|
156
184
|
config: process.env['CONFIG_PATH'],
|
|
157
185
|
projectRoot: process.env['PROJECT_ROOT'] ?? process.env['PWD']
|
|
158
186
|
}).catch((error: unknown) => {
|
|
159
|
-
|
|
187
|
+
logger.error({ error: error instanceof Error ? error.message : String(error) }, 'Failed to start MCP server');
|
|
160
188
|
process.exit(1);
|
|
161
189
|
});
|
|
162
190
|
}
|
|
@@ -17,6 +17,19 @@ export interface Chunk {
|
|
|
17
17
|
docSummary?: string | undefined;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Preset configurations for different content types.
|
|
22
|
+
* Code uses smaller chunks for precise symbol matching.
|
|
23
|
+
* Web/docs use larger chunks to preserve prose context.
|
|
24
|
+
*/
|
|
25
|
+
const CHUNK_PRESETS = {
|
|
26
|
+
code: { chunkSize: 768, chunkOverlap: 100 },
|
|
27
|
+
web: { chunkSize: 1200, chunkOverlap: 200 },
|
|
28
|
+
docs: { chunkSize: 1200, chunkOverlap: 200 },
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type ContentType = keyof typeof CHUNK_PRESETS;
|
|
32
|
+
|
|
20
33
|
export class ChunkingService {
|
|
21
34
|
private readonly chunkSize: number;
|
|
22
35
|
private readonly chunkOverlap: number;
|
|
@@ -26,6 +39,16 @@ export class ChunkingService {
|
|
|
26
39
|
this.chunkOverlap = config.chunkOverlap;
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Create a ChunkingService with preset configuration for a content type.
|
|
44
|
+
* - 'code': Smaller chunks (768/100) for precise code symbol matching
|
|
45
|
+
* - 'web': Larger chunks (1200/200) for web prose content
|
|
46
|
+
* - 'docs': Larger chunks (1200/200) for documentation
|
|
47
|
+
*/
|
|
48
|
+
static forContentType(type: ContentType): ChunkingService {
|
|
49
|
+
return new ChunkingService(CHUNK_PRESETS[type]);
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
/**
|
|
30
53
|
* Chunk text content. Uses semantic chunking for Markdown and code files,
|
|
31
54
|
* falling back to sliding window for other content.
|