@code-context-engine/mcp 0.1.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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @code-context-engine/mcp
2
+
3
+ Remote MCP server for [Context Engine](https://codesay.ai). Exposes a single tool, `codebase_context`, for Claude Code, Cursor, Windsurf, and any MCP client.
4
+
5
+ It syncs your local repo incrementally to the remote Context Engine, runs hybrid retrieval (semantic + keyword + symbol graph), and returns a token-budgeted context pack.
6
+
7
+ ## Quick start
8
+
9
+ 1. Sign in at [codesay.ai](https://codesay.ai) with GitHub and copy your `cek_…` API key from the console.
10
+ 2. Add this to your MCP config:
11
+
12
+ ```json
13
+ {
14
+ "mcpServers": {
15
+ "context-engine": {
16
+ "command": "npx",
17
+ "args": ["-y", "@code-context-engine/mcp"],
18
+ "env": {
19
+ "CONTEXT_ENGINE_REMOTE_API_KEY": "<your cek_ key>"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ The remote service URL defaults to `https://api.codesay.ai`. Override with `CONTEXT_ENGINE_REMOTE_URL` for self-hosted deployments.
27
+
28
+ ## Environment variables
29
+
30
+ | Variable | Required | Default | Description |
31
+ |----------|----------|---------|-------------|
32
+ | `CONTEXT_ENGINE_REMOTE_API_KEY` | yes | — | User API key (`cek_…`) from the web console |
33
+ | `CONTEXT_ENGINE_REMOTE_URL` | no | `https://api.codesay.ai` | Remote HTTP service base URL |
34
+ | `CONTEXT_ENGINE_REMOTE_MAX_FILE_BYTES` | no | `1048576` | Skip local files larger than this when syncing |
35
+
36
+ ## Tool: `codebase_context`
37
+
38
+ Required arguments:
39
+
40
+ - `path` — absolute path to the local repository root
41
+ - `query` — natural language or symbol-oriented search query
42
+
43
+ Optional: `token_budget`, `mode` (`hybrid` / `semantic` / `keyword`), `ext_filter`, `path_prefix`, `max_files`, `feedback`.
44
+
45
+ Only retrieval calls that return a context pack count against your billing quota. File sync, indexing, and feedback do not.
46
+
47
+ ## Programmatic use
48
+
49
+ ```ts
50
+ import { createRemoteMcpServer, runCodebaseContext } from '@code-context-engine/mcp';
51
+ ```
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,96 @@
1
+ import type { MatchSource } from '@code-context-engine/shared';
2
+ export interface CodebaseContextInput {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ path: string;
6
+ query: string;
7
+ tokenBudget?: number;
8
+ mode?: 'hybrid' | 'semantic' | 'keyword';
9
+ extFilter?: string[];
10
+ pathPrefix?: string[];
11
+ maxFiles?: number;
12
+ ignoreGlobs?: string[];
13
+ forceIndex?: boolean;
14
+ /** 本地文件发现的单文件大小上限(字节),应与远程服务的 CONTEXT_ENGINE_REMOTE_MAX_FILE_BYTES 一致——
15
+ * 否则本地判断"该传"的文件会在上传时被服务端按 413 拒绝。 */
16
+ maxFileBytes?: number;
17
+ feedback?: CodebaseContextFeedbackInput;
18
+ }
19
+ export interface CodebaseContextFeedbackInput {
20
+ rating: 'good' | 'bad' | 'partial';
21
+ expectedFiles?: string[];
22
+ selectedFiles?: string[];
23
+ missingFiles?: string[];
24
+ note?: string;
25
+ requestId?: string;
26
+ metadata?: Record<string, string | number | boolean | null>;
27
+ }
28
+ export declare class CodebaseContextInputError extends Error {
29
+ constructor(message: string);
30
+ }
31
+ export type CodebaseContextMatchType = MatchSource;
32
+ export type CodebaseContextIndexStatus = 'ready' | 'indexing' | 'error';
33
+ export interface CodebaseContextSpan {
34
+ start: number;
35
+ end: number;
36
+ symbols: string[];
37
+ code: string;
38
+ /** true = 索引后文件已改动且无法重定位:code 为索引时快照,行号仅作大致定位参考。 */
39
+ stale?: boolean;
40
+ }
41
+ export interface CodebaseContextOutlineEntry {
42
+ kind: string;
43
+ name: string;
44
+ line: number;
45
+ }
46
+ export type CodebaseContextConfidence = 'high' | 'medium' | 'low';
47
+ export interface CodebaseContextResult {
48
+ file: string;
49
+ language: string;
50
+ /** 相对置信度分级(相对本次检索 top1)。刻意不透出内部 raw score:
51
+ * 量纲无意义、跨查询不可比,agent 无法正确消费浮点分,分级才是稳定契约。 */
52
+ confidence: CodebaseContextConfidence;
53
+ matchType: CodebaseContextMatchType;
54
+ spans: CodebaseContextSpan[];
55
+ outline?: CodebaseContextOutlineEntry[];
56
+ }
57
+ export interface CodebaseContextOutput {
58
+ tool: 'codebase_context';
59
+ query: string;
60
+ context: {
61
+ indexStatus: CodebaseContextIndexStatus;
62
+ query?: string;
63
+ mode?: 'hybrid' | 'semantic' | 'keyword';
64
+ tokensUsed?: number;
65
+ tokenBudget?: number;
66
+ truncated?: boolean;
67
+ results?: CodebaseContextResult[];
68
+ related?: {
69
+ likely_edit_targets: string[];
70
+ verification_targets: string[];
71
+ };
72
+ };
73
+ feedback: {
74
+ requestId: string;
75
+ submitted?: {
76
+ rating: 'good' | 'bad' | 'partial';
77
+ };
78
+ };
79
+ }
80
+ interface RemoteContextResponse {
81
+ state?: string;
82
+ path?: string;
83
+ processedFiles?: number;
84
+ totalFiles?: number;
85
+ query?: string;
86
+ mode?: 'hybrid' | 'semantic' | 'keyword';
87
+ tokensUsed?: number;
88
+ tokenBudget?: number;
89
+ truncated?: boolean;
90
+ results?: unknown;
91
+ related?: unknown;
92
+ }
93
+ export declare function runCodebaseContext(input: CodebaseContextInput): Promise<CodebaseContextOutput>;
94
+ export declare function normalizeContext(context: RemoteContextResponse): CodebaseContextOutput['context'];
95
+ export {};
96
+ //# sourceMappingURL=codebaseContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codebaseContext.d.ts","sourceRoot":"","sources":["../src/codebaseContext.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,206 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isAbsolute } from 'node:path';
3
+ import { RemoteApiClient } from './remoteClient.js';
4
+ import { syncLocalRepo } from './syncLocalRepo.js';
5
+ export class CodebaseContextInputError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'CodebaseContextInputError';
9
+ }
10
+ }
11
+ const INTERNAL_CONTEXT_KEYS = new Set([
12
+ 'activeTrainingModel',
13
+ 'active_training_model',
14
+ 'repoId',
15
+ 'repo_id',
16
+ 'trainingDataset',
17
+ 'trainingDatasets',
18
+ 'trainingJob',
19
+ 'trainingJobs',
20
+ 'trainingModel',
21
+ 'trainingModels',
22
+ 'training_dataset',
23
+ 'training_datasets',
24
+ 'training_job',
25
+ 'training_jobs',
26
+ 'training_model',
27
+ 'training_models',
28
+ 'workspaceId',
29
+ 'workspace_id',
30
+ ]);
31
+ export async function runCodebaseContext(input) {
32
+ if (!isAbsolute(input.path)) {
33
+ throw new CodebaseContextInputError('Use an absolute local repository path.');
34
+ }
35
+ const client = new RemoteApiClient({ baseUrl: input.baseUrl, apiKey: input.apiKey });
36
+ const sync = await syncLocalRepo(client, input.path, {
37
+ ignoreGlobs: input.ignoreGlobs,
38
+ forceIndex: input.forceIndex,
39
+ maxFileBytes: input.maxFileBytes,
40
+ });
41
+ const context = await client.context(sync.repoId, {
42
+ query: input.query,
43
+ mode: input.mode,
44
+ tokenBudget: input.tokenBudget,
45
+ maxFiles: input.maxFiles,
46
+ extFilter: input.extFilter,
47
+ pathPrefix: input.pathPrefix,
48
+ });
49
+ const requestId = input.feedback?.requestId ?? `ctx_${randomUUID().replace(/-/g, '')}`;
50
+ const submittedFeedback = input.feedback
51
+ ? await client.submitFeedback(sync.repoId, {
52
+ query: input.query,
53
+ rating: input.feedback.rating,
54
+ expectedFiles: input.feedback.expectedFiles,
55
+ selectedFiles: input.feedback.selectedFiles,
56
+ missingFiles: input.feedback.missingFiles,
57
+ note: input.feedback.note,
58
+ requestId,
59
+ metadata: input.feedback.metadata,
60
+ })
61
+ : null;
62
+ return {
63
+ tool: 'codebase_context',
64
+ query: input.query,
65
+ context: normalizeContext(context),
66
+ feedback: {
67
+ requestId,
68
+ submitted: submittedFeedback ? {
69
+ rating: submittedFeedback.rating,
70
+ } : undefined,
71
+ },
72
+ };
73
+ }
74
+ export function normalizeContext(context) {
75
+ if ('state' in context) {
76
+ return {
77
+ indexStatus: normalizeIndexStatus(context.state),
78
+ };
79
+ }
80
+ return {
81
+ indexStatus: 'ready',
82
+ query: context.query,
83
+ mode: context.mode,
84
+ tokensUsed: context.tokensUsed,
85
+ tokenBudget: context.tokenBudget,
86
+ truncated: context.truncated,
87
+ results: normalizeResults(context.results),
88
+ related: normalizeRelated(context.related),
89
+ };
90
+ }
91
+ function normalizeIndexStatus(state) {
92
+ if (state === 'done')
93
+ return 'ready';
94
+ if (state === 'error')
95
+ return 'error';
96
+ return 'indexing';
97
+ }
98
+ function normalizeResults(value) {
99
+ if (!Array.isArray(value))
100
+ return undefined;
101
+ return value
102
+ .map((item) => normalizeResult(item))
103
+ .filter((item) => item !== null);
104
+ }
105
+ function confidenceValue(value) {
106
+ return value === 'high' || value === 'medium' || value === 'low' ? value : null;
107
+ }
108
+ function normalizeResult(value) {
109
+ if (!isRecord(value))
110
+ return null;
111
+ const file = stringValue(value.file);
112
+ const matchType = matchTypeValue(value.matchType);
113
+ if (!file || !matchType)
114
+ return null;
115
+ // 旧版 server 无 confidence 字段:按位次已隐含排序,缺省归 medium(不因版本偏差丢结果)
116
+ const confidence = confidenceValue(value.confidence) ?? 'medium';
117
+ const outline = normalizeOutline(value.outline);
118
+ return {
119
+ file,
120
+ language: stringValue(value.language) ?? 'unknown',
121
+ confidence,
122
+ matchType,
123
+ spans: normalizeSpans(value.spans),
124
+ ...(outline ? { outline } : {}),
125
+ };
126
+ }
127
+ function normalizeOutline(value) {
128
+ if (!Array.isArray(value))
129
+ return undefined;
130
+ const entries = value
131
+ .map((item) => {
132
+ if (!isRecord(item))
133
+ return null;
134
+ const kind = stringValue(item.kind);
135
+ const name = stringValue(item.name);
136
+ const line = numberValue(item.line);
137
+ if (!kind || !name || line === undefined)
138
+ return null;
139
+ return { kind, name, line };
140
+ })
141
+ .filter((item) => item !== null);
142
+ return entries.length > 0 ? entries : undefined;
143
+ }
144
+ function normalizeSpans(value) {
145
+ if (!Array.isArray(value))
146
+ return [];
147
+ return value
148
+ .map((item) => normalizeSpan(item))
149
+ .filter((item) => item !== null);
150
+ }
151
+ function normalizeSpan(value) {
152
+ if (!isRecord(value))
153
+ return null;
154
+ const start = numberValue(value.start);
155
+ const end = numberValue(value.end);
156
+ const code = stringValue(value.code);
157
+ if (start === undefined || end === undefined || code === undefined)
158
+ return null;
159
+ return {
160
+ start,
161
+ end,
162
+ symbols: stringArray(value.symbols),
163
+ code,
164
+ ...(value.stale === true ? { stale: true } : {}),
165
+ };
166
+ }
167
+ function normalizeRelated(value) {
168
+ if (!value || typeof value !== 'object' || Array.isArray(value))
169
+ return undefined;
170
+ const related = sanitizeInternalContextFields(value);
171
+ return {
172
+ likely_edit_targets: stringArray(related.likely_edit_targets),
173
+ verification_targets: stringArray(related.verification_targets),
174
+ };
175
+ }
176
+ function stringArray(value) {
177
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
178
+ }
179
+ function isRecord(value) {
180
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
181
+ }
182
+ function stringValue(value) {
183
+ return typeof value === 'string' ? value : undefined;
184
+ }
185
+ function numberValue(value) {
186
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
187
+ }
188
+ function matchTypeValue(value) {
189
+ return value === 'dense' || value === 'sparse' || value === 'both' || value === 'graph' || value === 'symbol'
190
+ ? value
191
+ : undefined;
192
+ }
193
+ function sanitizeInternalContextFields(value) {
194
+ if (Array.isArray(value)) {
195
+ return value.map((item) => sanitizeInternalContextFields(item));
196
+ }
197
+ if (!value || typeof value !== 'object')
198
+ return value;
199
+ const out = {};
200
+ for (const [key, child] of Object.entries(value)) {
201
+ if (INTERNAL_CONTEXT_KEYS.has(key))
202
+ continue;
203
+ out[key] = sanitizeInternalContextFields(child);
204
+ }
205
+ return out;
206
+ }
@@ -0,0 +1,5 @@
1
+ export { createRemoteMcpServer, REMOTE_MCP_PUBLIC_TOOLS } from './remoteMcpServer.js';
2
+ export { runCodebaseContext, normalizeContext, CodebaseContextInputError, } from './codebaseContext.js';
3
+ export { RemoteApiClient, RemoteApiError, remoteQuotaErrorMessage, type ResolvedRemoteRepo } from './remoteClient.js';
4
+ export { syncLocalRepo, assertLocalRepoDirectory, repoKeyForPath, uploadBatchesParallel, SyncLocalRepoInputError, RemoteIndexError, } from './syncLocalRepo.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,uBAAuB,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACtH,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createRemoteMcpServer, REMOTE_MCP_PUBLIC_TOOLS } from './remoteMcpServer.js';
2
+ export { runCodebaseContext, normalizeContext, CodebaseContextInputError, } from './codebaseContext.js';
3
+ export { RemoteApiClient, RemoteApiError, remoteQuotaErrorMessage } from './remoteClient.js';
4
+ export { syncLocalRepo, assertLocalRepoDirectory, repoKeyForPath, uploadBatchesParallel, SyncLocalRepoInputError, RemoteIndexError, } from './syncLocalRepo.js';
@@ -0,0 +1,167 @@
1
+ import type { ActiveTrainingModel, ApprovedTrainingModel, EvalReport, FeedbackFailurePackage, FeedbackQualitySummary, FeedbackTrainingDatasetInfo, FeedbackTrainingExample, FileManifestEntry, ManifestDiff, QueryLabel, TrainingJob, TrainingJobType, TrainingJobUpdateStatus, UploadedFile } from '@code-context-engine/shared';
2
+ export interface RemoteApiClientOptions {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ /** 单次请求超时(毫秒)。远程服务网络黑洞(不返回 RST)时 fetch 本身不会超时,
6
+ * 没有这个兜底 codebase_context 工具调用会永久挂起。默认 30s。 */
7
+ timeoutMs?: number;
8
+ }
9
+ export declare class RemoteApiError extends Error {
10
+ readonly status: number;
11
+ readonly body: unknown;
12
+ constructor(status: number, body: unknown);
13
+ }
14
+ /**
15
+ * 供所有客户端(mcp-client 的 remoteMcpServer、server 包的 CLI)复用的 402 文案分派,
16
+ * 避免每个消费方各自维护一份错误码→文案映射。每次调用扣次的检索余额(quota_exhausted)
17
+ * 按月重置,引导"等重置";建仓/存储/上传/重建索引这四类是硬性上限,不会自动重置,
18
+ * 得引导"清理或升级"而不是"等重置"。无匹配码(含未来 server 新增的、或老版本纯
19
+ * quota_exhausted)回落到旧文案,保证向后兼容。
20
+ */
21
+ export declare function remoteQuotaErrorMessage(err: RemoteApiError): string;
22
+ export interface CreateRemoteRepoInput {
23
+ name: string;
24
+ rootHint: string;
25
+ clientRepoKey: string;
26
+ }
27
+ export interface ResolvedRemoteRepo {
28
+ workspaceId: string;
29
+ repoId: string;
30
+ name: string;
31
+ rootHint: string;
32
+ clientRepoKey: string;
33
+ }
34
+ export interface RemoteRepoResponse {
35
+ workspace_id: string;
36
+ repo_id: string;
37
+ name: string;
38
+ }
39
+ export interface RemoteContextRequest {
40
+ query: string;
41
+ mode?: 'hybrid' | 'semantic' | 'keyword';
42
+ tokenBudget?: number;
43
+ maxFiles?: number;
44
+ extFilter?: string[];
45
+ pathPrefix?: string[];
46
+ /** 计费幂等键;不传则自动生成。重试同一逻辑请求时应复用同一个值。 */
47
+ requestId?: string;
48
+ }
49
+ export interface RemoteFeedbackInput {
50
+ query: string;
51
+ rating: 'good' | 'bad' | 'partial';
52
+ expectedFiles?: string[];
53
+ selectedFiles?: string[];
54
+ missingFiles?: string[];
55
+ note?: string;
56
+ requestId?: string;
57
+ metadata?: Record<string, string | number | boolean | null>;
58
+ }
59
+ export interface RemoteFeedbackEvent {
60
+ feedbackId: string;
61
+ workspaceId: string;
62
+ repoId: string;
63
+ createdAt: string;
64
+ query: string;
65
+ rating: 'good' | 'bad' | 'partial';
66
+ expectedFiles?: string[];
67
+ selectedFiles?: string[];
68
+ missingFiles?: string[];
69
+ note?: string;
70
+ requestId?: string;
71
+ metadata?: Record<string, string | number | boolean | null>;
72
+ }
73
+ export interface RemoteFeedbackEvalResult {
74
+ workspaceId: string;
75
+ repoId: string;
76
+ reportPath: string;
77
+ labels: QueryLabel[];
78
+ off: EvalReport;
79
+ on: EvalReport;
80
+ quality: FeedbackQualitySummary;
81
+ }
82
+ export declare class RemoteApiClient {
83
+ private opts;
84
+ private baseUrl;
85
+ private timeoutMs;
86
+ constructor(opts: RemoteApiClientOptions);
87
+ createRepo(input: CreateRemoteRepoInput): Promise<{
88
+ workspaceId: string;
89
+ repoId: string;
90
+ name: string;
91
+ }>;
92
+ getRepoByClientRepoKey(clientRepoKey: string): Promise<{
93
+ workspaceId: string;
94
+ repoId: string;
95
+ name: string;
96
+ } | null>;
97
+ resolveRepoByPath(path: string): Promise<ResolvedRemoteRepo | null>;
98
+ diffManifest(repoId: string, files: FileManifestEntry[]): Promise<ManifestDiff>;
99
+ health(): Promise<{
100
+ status: string;
101
+ }>;
102
+ uploadFiles(repoId: string, files: UploadedFile[]): Promise<{
103
+ status: string;
104
+ }>;
105
+ deleteFiles(repoId: string, files: string[]): Promise<{
106
+ status: string;
107
+ deleted: number;
108
+ }>;
109
+ indexRepo(repoId: string, force?: boolean): Promise<unknown>;
110
+ getIndexStatus(repoId: string): Promise<unknown>;
111
+ context(repoId: string, input: RemoteContextRequest): Promise<any>;
112
+ submitFeedback(repoId: string, input: RemoteFeedbackInput): Promise<RemoteFeedbackEvent>;
113
+ listFeedback(repoId: string, limit?: number): Promise<RemoteFeedbackEvent[]>;
114
+ listFeedbackLabels(repoId: string, limit?: number): Promise<QueryLabel[]>;
115
+ listFeedbackTrainingExamples(repoId: string, limit?: number): Promise<FeedbackTrainingExample[]>;
116
+ exportFeedbackTrainingDataset(repoId: string, input?: {
117
+ limit?: number;
118
+ datasetId?: string;
119
+ }): Promise<{
120
+ datasetPath: string;
121
+ numExamples: number;
122
+ }>;
123
+ listFeedbackTrainingDatasets(repoId: string): Promise<FeedbackTrainingDatasetInfo[]>;
124
+ createTrainingJob(repoId: string, input: {
125
+ datasetPath: string;
126
+ jobType: TrainingJobType;
127
+ modelHint?: string;
128
+ }): Promise<TrainingJob>;
129
+ listTrainingJobs(repoId: string): Promise<TrainingJob[]>;
130
+ listApprovedTrainingModels(repoId: string): Promise<ApprovedTrainingModel[]>;
131
+ activateTrainingModel(repoId: string, jobId: string, input?: {
132
+ activatedBy?: string;
133
+ }): Promise<ActiveTrainingModel>;
134
+ getActiveTrainingModel(repoId: string): Promise<ActiveTrainingModel | null>;
135
+ listTrainingModelActivations(repoId: string): Promise<ActiveTrainingModel[]>;
136
+ claimTrainingJob(repoId: string, input?: {
137
+ workerId?: string;
138
+ }): Promise<TrainingJob | null>;
139
+ releaseStaleTrainingJobs(repoId: string, input: {
140
+ maxRunningMs: number;
141
+ }): Promise<TrainingJob[]>;
142
+ heartbeatTrainingJob(repoId: string, jobId: string, input?: {
143
+ workerId?: string;
144
+ }): Promise<TrainingJob>;
145
+ updateTrainingJobStatus(repoId: string, jobId: string, input: {
146
+ status: TrainingJobUpdateStatus;
147
+ outputModelPath?: string;
148
+ metrics?: Record<string, number>;
149
+ error?: string;
150
+ qualityGate?: {
151
+ minEvalRecallAt10?: number;
152
+ };
153
+ workerId?: string;
154
+ }): Promise<TrainingJob>;
155
+ runFeedbackEval(repoId: string, input?: {
156
+ limit?: number;
157
+ maxRecallDrop?: number;
158
+ }): Promise<RemoteFeedbackEvalResult>;
159
+ getFeedbackFailures(repoId: string, input?: {
160
+ reportPath?: string;
161
+ maxItems?: number;
162
+ minRecallAt10?: number;
163
+ }): Promise<FeedbackFailurePackage>;
164
+ private request;
165
+ private get;
166
+ }
167
+ //# sourceMappingURL=remoteClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteClient.d.ts","sourceRoot":"","sources":["../src/remoteClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,334 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class RemoteApiError extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body) {
6
+ super(`Remote API ${status}`);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.name = 'RemoteApiError';
10
+ }
11
+ }
12
+ const QUOTA_EXCEEDED_MESSAGES = {
13
+ repo_quota_exceeded: 'Remote Context Engine request failed: repo_quota_exceeded. '
14
+ + 'The workspace has reached its repository limit. Delete an unused repository or upgrade the subscription, then retry.',
15
+ storage_quota_exceeded: 'Remote Context Engine request failed: storage_quota_exceeded. '
16
+ + 'The workspace has reached its storage limit. Delete unused files or repositories, or upgrade the subscription, then retry.',
17
+ upload_quota_exceeded: 'Remote Context Engine request failed: upload_quota_exceeded. '
18
+ + 'The workspace has reached today\'s upload limit. Retry after the daily reset or upgrade the subscription.',
19
+ index_quota_exceeded: 'Remote Context Engine request failed: index_quota_exceeded. '
20
+ + 'The workspace has reached today\'s force-reindex limit. Retry after the daily reset or upgrade the subscription.',
21
+ };
22
+ /**
23
+ * 供所有客户端(mcp-client 的 remoteMcpServer、server 包的 CLI)复用的 402 文案分派,
24
+ * 避免每个消费方各自维护一份错误码→文案映射。每次调用扣次的检索余额(quota_exhausted)
25
+ * 按月重置,引导"等重置";建仓/存储/上传/重建索引这四类是硬性上限,不会自动重置,
26
+ * 得引导"清理或升级"而不是"等重置"。无匹配码(含未来 server 新增的、或老版本纯
27
+ * quota_exhausted)回落到旧文案,保证向后兼容。
28
+ */
29
+ export function remoteQuotaErrorMessage(err) {
30
+ const body = err.body;
31
+ const code = typeof body?.error === 'string' ? body.error : undefined;
32
+ if (code && QUOTA_EXCEEDED_MESSAGES[code])
33
+ return QUOTA_EXCEEDED_MESSAGES[code];
34
+ return 'Remote Context Engine request failed: quota_exhausted. '
35
+ + 'The workspace has no retrieval quota left for this billing period. '
36
+ + 'Wait for the monthly reset or upgrade the subscription in the Context Engine console.';
37
+ }
38
+ function mapFeedback(r) {
39
+ return {
40
+ feedbackId: r.feedback_id,
41
+ workspaceId: r.workspace_id,
42
+ repoId: r.repo_id,
43
+ createdAt: r.created_at,
44
+ query: r.query,
45
+ rating: r.rating,
46
+ expectedFiles: r.expected_files,
47
+ selectedFiles: r.selected_files,
48
+ missingFiles: r.missing_files,
49
+ note: r.note,
50
+ requestId: r.request_id,
51
+ metadata: r.metadata,
52
+ };
53
+ }
54
+ function mapFeedbackEval(r) {
55
+ return {
56
+ workspaceId: r.workspace_id,
57
+ repoId: r.repo_id,
58
+ reportPath: r.report_path,
59
+ labels: r.labels,
60
+ off: r.off,
61
+ on: r.on,
62
+ quality: r.quality,
63
+ };
64
+ }
65
+ function mapTrainingJob(r) {
66
+ return {
67
+ jobId: r.job_id,
68
+ workspaceId: r.workspace_id,
69
+ repoId: r.repo_id,
70
+ datasetPath: r.dataset_path,
71
+ jobType: r.job_type,
72
+ status: r.status,
73
+ modelHint: r.model_hint,
74
+ createdAt: r.created_at,
75
+ updatedAt: r.updated_at,
76
+ startedAt: r.started_at,
77
+ finishedAt: r.finished_at,
78
+ outputModelPath: r.output_model_path,
79
+ metrics: r.metrics,
80
+ error: r.error,
81
+ claimedBy: r.claimed_by,
82
+ attempts: r.attempts,
83
+ lastHeartbeatAt: r.last_heartbeat_at,
84
+ quality: r.quality,
85
+ };
86
+ }
87
+ function mapApprovedTrainingModel(r) {
88
+ return {
89
+ jobId: r.job_id,
90
+ workspaceId: r.workspace_id,
91
+ repoId: r.repo_id,
92
+ datasetPath: r.dataset_path,
93
+ jobType: r.job_type,
94
+ modelPath: r.model_path,
95
+ modelHint: r.model_hint,
96
+ metrics: r.metrics,
97
+ quality: r.quality,
98
+ createdAt: r.created_at,
99
+ updatedAt: r.updated_at,
100
+ finishedAt: r.finished_at,
101
+ };
102
+ }
103
+ function mapActiveTrainingModel(r) {
104
+ return {
105
+ ...mapApprovedTrainingModel(r),
106
+ activatedAt: r.activated_at,
107
+ activatedBy: r.activated_by,
108
+ };
109
+ }
110
+ const DEFAULT_TIMEOUT_MS = 30_000;
111
+ export class RemoteApiClient {
112
+ opts;
113
+ baseUrl;
114
+ timeoutMs;
115
+ constructor(opts) {
116
+ this.opts = opts;
117
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
118
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
119
+ }
120
+ createRepo(input) {
121
+ return this.request('/v1/repos', {
122
+ name: input.name,
123
+ root_hint: input.rootHint,
124
+ client_repo_key: input.clientRepoKey,
125
+ }).then((r) => ({ workspaceId: r.workspace_id, repoId: r.repo_id, name: r.name }));
126
+ }
127
+ async getRepoByClientRepoKey(clientRepoKey) {
128
+ try {
129
+ const r = await this.get(`/v1/repos/by-client-key?client_repo_key=${encodeURIComponent(clientRepoKey)}`);
130
+ return { workspaceId: r.workspace_id, repoId: r.repo_id, name: r.name };
131
+ }
132
+ catch (err) {
133
+ if (err instanceof RemoteApiError && err.status === 404)
134
+ return null;
135
+ throw err;
136
+ }
137
+ }
138
+ async resolveRepoByPath(path) {
139
+ try {
140
+ const r = await this.get(`/v1/repos/resolve?path=${encodeURIComponent(path)}`);
141
+ return {
142
+ workspaceId: r.workspace_id,
143
+ repoId: r.repo_id,
144
+ name: r.name,
145
+ rootHint: r.root_hint,
146
+ clientRepoKey: r.client_repo_key,
147
+ };
148
+ }
149
+ catch (err) {
150
+ if (err instanceof RemoteApiError && err.status === 404)
151
+ return null;
152
+ throw err;
153
+ }
154
+ }
155
+ diffManifest(repoId, files) {
156
+ return this.request(`/v1/repos/${repoId}/manifest`, { files });
157
+ }
158
+ health() {
159
+ return this.get('/v1/health');
160
+ }
161
+ uploadFiles(repoId, files) {
162
+ return this.request(`/v1/repos/${repoId}/files:batch`, { files });
163
+ }
164
+ deleteFiles(repoId, files) {
165
+ return this.request(`/v1/repos/${repoId}/files:delete`, { files });
166
+ }
167
+ indexRepo(repoId, force = false) {
168
+ return this.request(`/v1/repos/${repoId}/index`, { force });
169
+ }
170
+ getIndexStatus(repoId) {
171
+ return this.get(`/v1/repos/${repoId}/index/status`);
172
+ }
173
+ async context(repoId, input) {
174
+ const path = `/v1/repos/${repoId}/context`;
175
+ const payload = {
176
+ query: input.query,
177
+ mode: input.mode,
178
+ token_budget: input.tokenBudget,
179
+ max_files: input.maxFiles,
180
+ ext_filter: input.extFilter,
181
+ path_prefix: input.pathPrefix,
182
+ // 计费幂等键:每次逻辑调用一个,重放/重试携带同一个则服务端不重复扣次
183
+ request_id: input.requestId ?? `ctxreq_${randomUUID()}`,
184
+ };
185
+ try {
186
+ return await this.request(path, payload);
187
+ }
188
+ catch (err) {
189
+ // 版本偏差兜底:升级后的 client 指向未升级的旧 server 时,旧 server 的 ContextSchema
190
+ // 是 .strict() 且没有 request_id 字段,会以 400 拒绝这个未知键。去掉该字段重试一次——
191
+ // 牺牲本次的计费幂等(旧 server 本就不支持),换取新 client 对旧 server 的可用性。
192
+ if (err instanceof RemoteApiError && err.status === 400
193
+ && JSON.stringify(err.body ?? '').includes('request_id')) {
194
+ const { request_id: _omit, ...legacy } = payload;
195
+ return await this.request(path, legacy);
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+ submitFeedback(repoId, input) {
201
+ return this.request(`/v1/repos/${repoId}/feedback`, {
202
+ query: input.query,
203
+ rating: input.rating,
204
+ expected_files: input.expectedFiles,
205
+ selected_files: input.selectedFiles,
206
+ missing_files: input.missingFiles,
207
+ note: input.note,
208
+ request_id: input.requestId,
209
+ metadata: input.metadata,
210
+ }).then(mapFeedback);
211
+ }
212
+ async listFeedback(repoId, limit = 100) {
213
+ const data = await this.get(`/v1/repos/${repoId}/feedback?limit=${encodeURIComponent(String(limit))}`);
214
+ return data.events.map(mapFeedback);
215
+ }
216
+ async listFeedbackLabels(repoId, limit = 100) {
217
+ const data = await this.get(`/v1/repos/${repoId}/feedback/labels?limit=${encodeURIComponent(String(limit))}`);
218
+ return data.labels;
219
+ }
220
+ async listFeedbackTrainingExamples(repoId, limit = 100) {
221
+ const data = await this.get(`/v1/repos/${repoId}/feedback/training?limit=${encodeURIComponent(String(limit))}`);
222
+ return data.examples;
223
+ }
224
+ exportFeedbackTrainingDataset(repoId, input = {}) {
225
+ return this.request(`/v1/repos/${repoId}/feedback/training/export`, {
226
+ limit: input.limit,
227
+ dataset_id: input.datasetId,
228
+ }).then((r) => ({ datasetPath: r.dataset_path, numExamples: r.num_examples }));
229
+ }
230
+ async listFeedbackTrainingDatasets(repoId) {
231
+ const data = await this.get(`/v1/repos/${repoId}/feedback/training/datasets`);
232
+ return data.datasets;
233
+ }
234
+ createTrainingJob(repoId, input) {
235
+ return this.request(`/v1/repos/${repoId}/training/jobs`, {
236
+ dataset_path: input.datasetPath,
237
+ job_type: input.jobType,
238
+ model_hint: input.modelHint,
239
+ }).then(mapTrainingJob);
240
+ }
241
+ async listTrainingJobs(repoId) {
242
+ const data = await this.get(`/v1/repos/${repoId}/training/jobs`);
243
+ return data.jobs.map(mapTrainingJob);
244
+ }
245
+ async listApprovedTrainingModels(repoId) {
246
+ const data = await this.get(`/v1/repos/${repoId}/training/models/approved`);
247
+ return data.models.map(mapApprovedTrainingModel);
248
+ }
249
+ activateTrainingModel(repoId, jobId, input = {}) {
250
+ return this.request(`/v1/repos/${repoId}/training/models/${jobId}/activate`, {
251
+ activated_by: input.activatedBy,
252
+ }).then(mapActiveTrainingModel);
253
+ }
254
+ async getActiveTrainingModel(repoId) {
255
+ const data = await this.get(`/v1/repos/${repoId}/training/models/active`);
256
+ return data.model ? mapActiveTrainingModel(data.model) : null;
257
+ }
258
+ async listTrainingModelActivations(repoId) {
259
+ const data = await this.get(`/v1/repos/${repoId}/training/models/activations`);
260
+ return data.activations.map(mapActiveTrainingModel);
261
+ }
262
+ claimTrainingJob(repoId, input = {}) {
263
+ return this.request(`/v1/repos/${repoId}/training/jobs:claim`, {
264
+ worker_id: input.workerId,
265
+ }).then((r) => (r.job ? mapTrainingJob(r.job) : null));
266
+ }
267
+ releaseStaleTrainingJobs(repoId, input) {
268
+ return this.request(`/v1/repos/${repoId}/training/jobs:release-stale`, {
269
+ max_running_ms: input.maxRunningMs,
270
+ }).then((r) => r.jobs.map(mapTrainingJob));
271
+ }
272
+ heartbeatTrainingJob(repoId, jobId, input = {}) {
273
+ return this.request(`/v1/repos/${repoId}/training/jobs/${jobId}/heartbeat`, {
274
+ worker_id: input.workerId,
275
+ }).then(mapTrainingJob);
276
+ }
277
+ updateTrainingJobStatus(repoId, jobId, input) {
278
+ return this.request(`/v1/repos/${repoId}/training/jobs/${jobId}/status`, {
279
+ status: input.status,
280
+ output_model_path: input.outputModelPath,
281
+ metrics: input.metrics,
282
+ error: input.error,
283
+ worker_id: input.workerId,
284
+ quality_gate: input.qualityGate ? {
285
+ min_eval_recall_at_10: input.qualityGate.minEvalRecallAt10,
286
+ } : undefined,
287
+ }).then(mapTrainingJob);
288
+ }
289
+ runFeedbackEval(repoId, input = {}) {
290
+ return this.request(`/v1/repos/${repoId}/eval/feedback`, {
291
+ limit: input.limit,
292
+ max_recall_drop: input.maxRecallDrop,
293
+ }).then(mapFeedbackEval);
294
+ }
295
+ getFeedbackFailures(repoId, input = {}) {
296
+ const params = new URLSearchParams();
297
+ if (input.reportPath)
298
+ params.set('report_path', input.reportPath);
299
+ if (input.maxItems !== undefined)
300
+ params.set('max_items', String(input.maxItems));
301
+ if (input.minRecallAt10 !== undefined)
302
+ params.set('min_recall_at_10', String(input.minRecallAt10));
303
+ const qs = params.toString();
304
+ return this.get(`/v1/repos/${repoId}/eval/feedback/failures${qs ? `?${qs}` : ''}`);
305
+ }
306
+ async request(path, body) {
307
+ const res = await fetch(`${this.baseUrl}${path}`, {
308
+ method: 'POST',
309
+ headers: {
310
+ authorization: `Bearer ${this.opts.apiKey}`,
311
+ 'content-type': 'application/json',
312
+ },
313
+ body: JSON.stringify(body),
314
+ signal: AbortSignal.timeout(this.timeoutMs),
315
+ });
316
+ const data = await res.json().catch(() => ({}));
317
+ if (!res.ok)
318
+ throw new RemoteApiError(res.status, data);
319
+ return data;
320
+ }
321
+ async get(path) {
322
+ const res = await fetch(`${this.baseUrl}${path}`, {
323
+ method: 'GET',
324
+ headers: {
325
+ authorization: `Bearer ${this.opts.apiKey}`,
326
+ },
327
+ signal: AbortSignal.timeout(this.timeoutMs),
328
+ });
329
+ const data = await res.json().catch(() => ({}));
330
+ if (!res.ok)
331
+ throw new RemoteApiError(res.status, data);
332
+ return data;
333
+ }
334
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ export declare const REMOTE_MCP_PUBLIC_TOOLS: readonly ["codebase_context"];
4
+ export declare function createRemoteMcpServer(): McpServer;
5
+ //# sourceMappingURL=remoteMcpServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteMcpServer.d.ts","sourceRoot":"","sources":["../src/remoteMcpServer.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAepE,eAAO,MAAM,uBAAuB,+BAAgC,CAAC;AAgGrE,wBAAgB,qBAAqB,cAyDpC"}
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { isAbsolute } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { loadConfig, resolveRemoteUrl } from '@code-context-engine/shared';
8
+ import { CodebaseContextInputError, runCodebaseContext } from './codebaseContext.js';
9
+ import { RemoteApiError, remoteQuotaErrorMessage } from './remoteClient.js';
10
+ import { SyncLocalRepoInputError } from './syncLocalRepo.js';
11
+ const ok = (data) => ({
12
+ content: [{ type: 'text', text: JSON.stringify(data) }],
13
+ structuredContent: data,
14
+ });
15
+ export const REMOTE_MCP_PUBLIC_TOOLS = ['codebase_context'];
16
+ const codebaseContextSpanSchema = z.object({
17
+ start: z.number(),
18
+ end: z.number(),
19
+ symbols: z.array(z.string()),
20
+ code: z.string(),
21
+ // 索引后文件已改动且无法重定位:code 为索引时快照,行号仅作大致定位参考
22
+ stale: z.boolean().optional(),
23
+ }).strict();
24
+ const codebaseContextOutlineSchema = z.object({
25
+ kind: z.string(),
26
+ name: z.string(),
27
+ line: z.number(),
28
+ }).strict();
29
+ const codebaseContextResultSchema = z.object({
30
+ file: z.string(),
31
+ language: z.string(),
32
+ confidence: z.enum(['high', 'medium', 'low']),
33
+ matchType: z.enum(['dense', 'sparse', 'both', 'graph', 'symbol']),
34
+ spans: z.array(codebaseContextSpanSchema),
35
+ outline: z.array(codebaseContextOutlineSchema).optional(),
36
+ }).strict();
37
+ const codebaseContextRelatedSchema = z.object({
38
+ likely_edit_targets: z.array(z.string()),
39
+ verification_targets: z.array(z.string()),
40
+ }).strict();
41
+ const codebaseContextIndexStatusSchema = z.enum(['ready', 'indexing', 'error']);
42
+ const codebaseContextInputSchema = z.object({
43
+ path: z.string().refine((value) => isAbsolute(value), {
44
+ message: 'Use an absolute local repository path.',
45
+ }).describe('本地代码库绝对路径'),
46
+ query: z.string().describe('自然语言或代码意图查询。Write the query in English for best retrieval quality; CJK queries are auto-translated before search (adding latency), except in keyword mode which searches the literal text.'),
47
+ token_budget: z.number().int().positive().optional().describe('返回内容总 token 上限, 默认 8000'),
48
+ mode: z.enum(['hybrid', 'semantic', 'keyword']).optional().describe('默认 hybrid'),
49
+ ext_filter: z.array(z.string()).optional().describe("限定扩展名, 如 ['.py','.ts']"),
50
+ path_prefix: z.array(z.string()).optional().describe("限定仓库相对路径前缀, 如 ['packages/server/src']"),
51
+ max_files: z.number().int().positive().optional().describe('返回文件数上限, 默认 20'),
52
+ feedback: z.object({
53
+ rating: z.enum(['good', 'bad', 'partial']).describe('本次上下文结果质量反馈'),
54
+ request_id: z.string().optional().describe('上一次 codebase_context 返回的 feedback.requestId'),
55
+ }).strict().optional().describe('可选结果质量反馈; 通过同一个 codebase_context 工具写回服务端'),
56
+ }).strict();
57
+ const codebaseContextOutputSchema = z.object({
58
+ tool: z.literal('codebase_context'),
59
+ query: z.string(),
60
+ context: z.object({
61
+ indexStatus: codebaseContextIndexStatusSchema,
62
+ query: z.string().optional(),
63
+ mode: z.enum(['hybrid', 'semantic', 'keyword']).optional(),
64
+ tokensUsed: z.number().optional(),
65
+ tokenBudget: z.number().optional(),
66
+ truncated: z.boolean().optional(),
67
+ results: z.array(codebaseContextResultSchema).optional(),
68
+ related: codebaseContextRelatedSchema.optional(),
69
+ }).strict(),
70
+ feedback: z.object({
71
+ requestId: z.string(),
72
+ submitted: z.object({
73
+ rating: z.enum(['good', 'bad', 'partial']),
74
+ }).strict().optional(),
75
+ }).strict(),
76
+ }).strict();
77
+ function publicRemoteErrorMessage(err) {
78
+ if (err instanceof CodebaseContextInputError || err instanceof SyncLocalRepoInputError) {
79
+ return `codebase_context input invalid: ${err.message}`;
80
+ }
81
+ if (err instanceof RemoteApiError) {
82
+ if (err.status === 401 || err.status === 403) {
83
+ return 'Remote Context Engine request failed: authentication_failed. Check the local MCP API key configuration.';
84
+ }
85
+ if (err.status === 402) {
86
+ return remoteQuotaErrorMessage(err);
87
+ }
88
+ if (err.status === 413) {
89
+ return 'Remote Context Engine request failed: payload_too_large. Reduce ignored files or split the repository, then retry.';
90
+ }
91
+ if (err.status === 400) {
92
+ return 'Remote Context Engine request failed: invalid_request. Check the local MCP input and repository sync settings.';
93
+ }
94
+ if (err.status >= 500) {
95
+ return 'Remote Context Engine request failed: server_error. Try again or contact the server operator.';
96
+ }
97
+ }
98
+ return 'Remote Context Engine request failed: unavailable. Check the remote service and network, then retry.';
99
+ }
100
+ export function createRemoteMcpServer() {
101
+ const cfg = loadConfig();
102
+ const server = new McpServer({ name: 'context-engine-remote', version: '0.1.0' });
103
+ server.registerTool(REMOTE_MCP_PUBLIC_TOOLS[0], {
104
+ description: '同步本地代码库到远程 Context Engine, 并返回按文件聚合、带 token 预算的上下文包。用于需要理解代码位置、调用关系或修改目标的查询。',
105
+ inputSchema: codebaseContextInputSchema,
106
+ outputSchema: codebaseContextOutputSchema,
107
+ annotations: {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ idempotentHint: false,
111
+ openWorldHint: true,
112
+ },
113
+ }, async ({ path, query, token_budget, mode, ext_filter, path_prefix, max_files, feedback, }) => {
114
+ const baseUrl = resolveRemoteUrl();
115
+ const key = cfg.remote.apiKey;
116
+ if (!key)
117
+ throw new Error('Remote Context Engine is not configured. Contact the server operator.');
118
+ try {
119
+ const out = await runCodebaseContext({
120
+ baseUrl,
121
+ apiKey: key,
122
+ path,
123
+ query,
124
+ tokenBudget: token_budget,
125
+ mode,
126
+ extFilter: ext_filter,
127
+ pathPrefix: path_prefix,
128
+ maxFiles: max_files,
129
+ maxFileBytes: cfg.remote.maxFileBytes,
130
+ feedback: feedback ? {
131
+ rating: feedback.rating,
132
+ requestId: feedback.request_id,
133
+ } : undefined,
134
+ });
135
+ return ok(out);
136
+ }
137
+ catch (err) {
138
+ throw new Error(publicRemoteErrorMessage(err));
139
+ }
140
+ });
141
+ return server;
142
+ }
143
+ async function main() {
144
+ await createRemoteMcpServer().connect(new StdioServerTransport());
145
+ }
146
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
147
+ main().catch((err) => {
148
+ console.error(err);
149
+ process.exit(1);
150
+ });
151
+ }
@@ -0,0 +1,37 @@
1
+ import { type UploadedFile } from '@code-context-engine/shared';
2
+ import type { RemoteApiClient } from './remoteClient.js';
3
+ export interface SyncLocalRepoOptions {
4
+ ignoreGlobs?: string[];
5
+ forceIndex?: boolean;
6
+ clientRepoKey?: string;
7
+ name?: string;
8
+ /** 应与远程服务的单文件大小上限一致,否则本地认为"该传"的文件会被服务端按 413 拒绝。 */
9
+ maxFileBytes?: number;
10
+ /** 等待远程索引完成的时间上限;超时不报错,带进行中状态返回。 */
11
+ waitForIndexMs?: number;
12
+ }
13
+ export interface SyncLocalRepoResult {
14
+ workspaceId: string;
15
+ repoId: string;
16
+ totalFiles: number;
17
+ uploaded: number;
18
+ deleted: number;
19
+ /** 返回时的远程索引状态:'done' 可立即检索;其余表示索引仍在后台进行。 */
20
+ indexState: string;
21
+ }
22
+ export declare class RemoteIndexError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ export declare class SyncLocalRepoInputError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ interface SizedUpload extends UploadedFile {
29
+ size: number;
30
+ }
31
+ export declare function syncLocalRepo(client: RemoteApiClient, repoPath: string, opts?: SyncLocalRepoOptions): Promise<SyncLocalRepoResult>;
32
+ export declare function repoKeyForPath(path: string): string;
33
+ export declare function assertLocalRepoDirectory(path: string): void;
34
+ /** @internal 导出供单测断言并行上传上限。 */
35
+ export declare function uploadBatchesParallel(client: RemoteApiClient, repoId: string, batches: SizedUpload[][]): Promise<void>;
36
+ export {};
37
+ //# sourceMappingURL=syncLocalRepo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncLocalRepo.d.ts","sourceRoot":"","sources":["../src/syncLocalRepo.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAWzD,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED,UAAU,WAAY,SAAQ,YAAY;IACxC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,mBAAmB,CAAC,CAwE9B;AAuCD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU3D;AAyBD,+BAA+B;AAC/B,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,WAAW,EAAE,EAAE,GACvB,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,190 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { basename, relative, resolve } from 'node:path';
4
+ import { discoverFiles } from '@code-context-engine/shared';
5
+ const MAX_UPLOAD_FILES_PER_BATCH = 1000;
6
+ const MAX_DELETE_FILES_PER_BATCH = 1000;
7
+ const MAX_UPLOAD_RAW_BYTES_PER_BATCH = 15_000_000;
8
+ /** 并行上传 batch 数上限(仍等全部 batch 完成后再 index)。 */
9
+ const UPLOAD_BATCH_CONCURRENCY = 4;
10
+ const INDEX_POLL_INTERVAL_MS = 500;
11
+ /** 低于 RemoteApiClient 单请求 30s 超时;冷启动大仓库等不完就带"索引中"状态返回,由 /context 的 IndexPending 信封接力。 */
12
+ const DEFAULT_WAIT_FOR_INDEX_MS = 20_000;
13
+ export class RemoteIndexError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'RemoteIndexError';
17
+ }
18
+ }
19
+ export class SyncLocalRepoInputError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = 'SyncLocalRepoInputError';
23
+ }
24
+ }
25
+ export async function syncLocalRepo(client, repoPath, opts = {}) {
26
+ let root = resolve(repoPath);
27
+ assertLocalRepoDirectory(root);
28
+ let repo;
29
+ const resolved = opts.clientRepoKey ? null : await resolveExistingRepo(client, root);
30
+ if (resolved) {
31
+ root = resolve(resolved.rootHint);
32
+ repo = resolved;
33
+ }
34
+ else {
35
+ repo = await client.createRepo({
36
+ name: opts.name ?? basename(root),
37
+ rootHint: root,
38
+ clientRepoKey: opts.clientRepoKey ?? repoKeyForPath(root),
39
+ });
40
+ }
41
+ const { files } = await discoverFiles(root, opts.ignoreGlobs, opts.maxFileBytes);
42
+ const manifest = files.map((full) => {
43
+ const content = readFileSync(full);
44
+ return {
45
+ path: relative(root, full).replace(/\\/g, '/'),
46
+ sha256: sha256(content),
47
+ size: content.length,
48
+ };
49
+ });
50
+ const diff = await client.diffManifest(repo.repoId, manifest);
51
+ const uploadSet = new Set([...diff.missing, ...diff.changed]);
52
+ const uploads = manifest
53
+ .filter((f) => uploadSet.has(f.path))
54
+ .map((f) => ({
55
+ path: f.path,
56
+ sha256: f.sha256,
57
+ size: f.size,
58
+ contentBase64: readFileSync(resolve(root, f.path)).toString('base64'),
59
+ }));
60
+ for (const batch of chunkItems(diff.deleted, MAX_DELETE_FILES_PER_BATCH)) {
61
+ await client.deleteFiles(repo.repoId, batch);
62
+ }
63
+ await uploadBatchesParallel(client, repo.repoId, chunkUploads(uploads));
64
+ // force 仅在调用方显式要求时使用:普通变更走服务端 contentHash 增量(只重嵌变更文件),
65
+ // 隐式 force 会导致"改一个文件 → 整仓重新 embedding"的成本放大(曾是 P0 级产品缺陷)。
66
+ const changed = uploads.length > 0 || diff.deleted.length > 0;
67
+ let indexState = 'idle';
68
+ let needStart = !!opts.forceIndex || changed;
69
+ if (!needStart) {
70
+ // 无变更时先看状态:'done' 直接跳过 /index(免掉每次调用的增量扫描税);
71
+ // 'idle' 含服务端重启丢内存状态的情况——磁盘索引若在,增量索引秒级空跑,必须触发以自愈
72
+ indexState = (await fetchIndexStatus(client, repo.repoId)).state;
73
+ needStart = indexState === 'idle' || indexState === 'error';
74
+ }
75
+ if (needStart) {
76
+ await client.indexRepo(repo.repoId, opts.forceIndex ?? false);
77
+ }
78
+ if (needStart || !isSettledIndexState(indexState)) {
79
+ const settled = await waitForIndexSettled(client, repo.repoId, opts.waitForIndexMs ?? DEFAULT_WAIT_FOR_INDEX_MS);
80
+ indexState = settled.state;
81
+ if (indexState === 'error') {
82
+ throw new RemoteIndexError(`Remote indexing failed: ${settled.error ?? 'unknown error'}`);
83
+ }
84
+ }
85
+ return {
86
+ workspaceId: repo.workspaceId,
87
+ repoId: repo.repoId,
88
+ totalFiles: manifest.length,
89
+ uploaded: uploads.length,
90
+ deleted: diff.deleted.length,
91
+ indexState,
92
+ };
93
+ }
94
+ function isSettledIndexState(state) {
95
+ return state === 'done' || state === 'error';
96
+ }
97
+ async function fetchIndexStatus(client, repoId) {
98
+ const raw = (await client.getIndexStatus(repoId));
99
+ return {
100
+ state: typeof raw?.state === 'string' ? raw.state : 'idle',
101
+ error: typeof raw?.error === 'string' ? raw.error : null,
102
+ };
103
+ }
104
+ /** 轮询远程索引状态直到进入稳态(done/error)或预算耗尽;超时按当前进行中状态返回,不视为失败。 */
105
+ async function waitForIndexSettled(client, repoId, budgetMs) {
106
+ const deadline = Date.now() + budgetMs;
107
+ for (;;) {
108
+ const status = await fetchIndexStatus(client, repoId);
109
+ if (isSettledIndexState(status.state))
110
+ return status;
111
+ const remaining = deadline - Date.now();
112
+ if (remaining <= 0)
113
+ return status;
114
+ await sleep(Math.min(INDEX_POLL_INTERVAL_MS, remaining));
115
+ }
116
+ }
117
+ function sleep(ms) {
118
+ return new Promise((resolve) => setTimeout(resolve, ms));
119
+ }
120
+ export function repoKeyForPath(path) {
121
+ return `local_${sha256(Buffer.from(path)).slice(0, 24)}`;
122
+ }
123
+ export function assertLocalRepoDirectory(path) {
124
+ let stat;
125
+ try {
126
+ stat = statSync(path);
127
+ }
128
+ catch {
129
+ throw new SyncLocalRepoInputError('Local repository path must exist and be a directory.');
130
+ }
131
+ if (!stat.isDirectory()) {
132
+ throw new SyncLocalRepoInputError('Local repository path must exist and be a directory.');
133
+ }
134
+ }
135
+ function chunkUploads(files) {
136
+ const batches = [];
137
+ let current = [];
138
+ let currentBytes = 0;
139
+ for (const file of files) {
140
+ if (current.length > 0
141
+ && (current.length >= MAX_UPLOAD_FILES_PER_BATCH
142
+ || currentBytes + file.size > MAX_UPLOAD_RAW_BYTES_PER_BATCH)) {
143
+ batches.push(current);
144
+ current = [];
145
+ currentBytes = 0;
146
+ }
147
+ current.push(file);
148
+ currentBytes += file.size;
149
+ }
150
+ if (current.length > 0)
151
+ batches.push(current);
152
+ return batches;
153
+ }
154
+ /** @internal 导出供单测断言并行上传上限。 */
155
+ export async function uploadBatchesParallel(client, repoId, batches) {
156
+ if (batches.length === 0)
157
+ return;
158
+ let next = 0;
159
+ async function worker() {
160
+ while (next < batches.length) {
161
+ const idx = next++;
162
+ const batch = batches[idx].map(({ size: _size, ...file }) => file);
163
+ await client.uploadFiles(repoId, batch);
164
+ }
165
+ }
166
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_BATCH_CONCURRENCY, batches.length) }, () => worker()));
167
+ }
168
+ function chunkItems(items, maxItems) {
169
+ const batches = [];
170
+ for (let i = 0; i < items.length; i += maxItems) {
171
+ batches.push(items.slice(i, i + maxItems));
172
+ }
173
+ return batches;
174
+ }
175
+ async function resolveExistingRepo(client, root) {
176
+ const resolved = await client.resolveRepoByPath(root);
177
+ if (!resolved)
178
+ return null;
179
+ try {
180
+ // rootHint 可能来自同一 workspace 的另一台机器,本地不存在则退回新建
181
+ assertLocalRepoDirectory(resolved.rootHint);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ return resolved;
187
+ }
188
+ function sha256(buf) {
189
+ return createHash('sha256').update(buf).digest('hex');
190
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@code-context-engine/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that gives Claude Code, Cursor, and any MCP client real codebase context without burning tokens — incremental sync, hybrid dense+sparse+symbol-graph retrieval, token-budgeted context packs.",
5
+ "license": "MIT",
6
+ "homepage": "https://codesay.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yeuxuan/context-engine.git",
10
+ "directory": "packages/mcp-client"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "scripts": {
16
+ "prepublishOnly": "tsc -b"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "mcp-server",
21
+ "claude-code",
22
+ "cursor",
23
+ "codebase-context",
24
+ "code-search",
25
+ "code-rag",
26
+ "semantic-code-search",
27
+ "token-budget"
28
+ ],
29
+ "type": "module",
30
+ "bin": {
31
+ "context-engine": "./dist/remoteMcpServer.js"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ }
38
+ },
39
+ "files": ["dist"],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@code-context-engine/shared": "^0.1.0",
45
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
+ "zod": "^3.23.8"
47
+ }
48
+ }