@mhalder/qdrant-mcp-server 1.6.0 → 2.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/.github/workflows/ci.yml +5 -16
- package/.nvmrc +1 -0
- package/CHANGELOG.md +30 -0
- package/README.md +86 -76
- package/build/embeddings/cohere.test.js +8 -6
- package/build/embeddings/cohere.test.js.map +1 -1
- package/build/embeddings/openai.test.js +12 -8
- package/build/embeddings/openai.test.js.map +1 -1
- package/build/index.js +26 -7
- package/build/index.js.map +1 -1
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +64 -24
- package/build/qdrant/client.test.js.map +1 -1
- package/compose.yaml +57 -0
- package/examples/README.md +5 -3
- package/package.json +22 -19
- package/src/embeddings/cohere.test.ts +9 -8
- package/src/embeddings/openai.test.ts +13 -10
- package/src/index.ts +144 -51
- package/src/qdrant/client.test.ts +200 -79
- package/src/qdrant/client.ts +25 -14
- package/tsconfig.json +5 -4
- package/vitest.config.ts +1 -0
- package/docker-compose.yml +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhalder/qdrant-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "MCP server for semantic search using local Qdrant and Ollama (default) with support for OpenAI, Cohere, and Voyage AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
],
|
|
26
26
|
"author": "mhalder",
|
|
27
27
|
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.0.0 <24.0.0"
|
|
30
|
+
},
|
|
28
31
|
"repository": {
|
|
29
32
|
"type": "git",
|
|
30
33
|
"url": "https://github.com/mhalder/qdrant-mcp-server.git"
|
|
@@ -37,38 +40,38 @@
|
|
|
37
40
|
"tree-sitter": "^0.25.0"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
41
|
-
"@qdrant/js-client-rest": "^1.
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
44
|
+
"@qdrant/js-client-rest": "^1.16.2",
|
|
42
45
|
"bottleneck": "^2.19.5",
|
|
43
|
-
"cohere-ai": "^7.
|
|
44
|
-
"express": "^5.1
|
|
46
|
+
"cohere-ai": "^7.20.0",
|
|
47
|
+
"express": "^5.2.1",
|
|
45
48
|
"ignore": "^7.0.5",
|
|
46
|
-
"openai": "^4.
|
|
49
|
+
"openai": "^4.104.0",
|
|
47
50
|
"tree-sitter": "^0.25.0",
|
|
48
|
-
"tree-sitter-bash": "^0.25.
|
|
51
|
+
"tree-sitter-bash": "^0.25.1",
|
|
49
52
|
"tree-sitter-go": "^0.25.0",
|
|
50
53
|
"tree-sitter-java": "^0.23.5",
|
|
51
54
|
"tree-sitter-javascript": "^0.25.0",
|
|
52
55
|
"tree-sitter-python": "^0.25.0",
|
|
53
56
|
"tree-sitter-rust": "^0.24.0",
|
|
54
57
|
"tree-sitter-typescript": "^0.23.2",
|
|
55
|
-
"zod": "^3.
|
|
58
|
+
"zod": "^3.25.76"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
|
-
"@commitlint/cli": "^20.1
|
|
59
|
-
"@commitlint/config-conventional": "^20.
|
|
61
|
+
"@commitlint/cli": "^20.3.1",
|
|
62
|
+
"@commitlint/config-conventional": "^20.3.1",
|
|
60
63
|
"@semantic-release/changelog": "^6.0.3",
|
|
61
64
|
"@semantic-release/git": "^10.0.1",
|
|
62
|
-
"@semantic-release/github": "^
|
|
63
|
-
"@semantic-release/npm": "^
|
|
64
|
-
"@types/express": "^5.0.
|
|
65
|
-
"@types/node": "^22.
|
|
66
|
-
"@vitest/coverage-v8": "^
|
|
67
|
-
"@vitest/ui": "^
|
|
65
|
+
"@semantic-release/github": "^12.0.2",
|
|
66
|
+
"@semantic-release/npm": "^13.1.3",
|
|
67
|
+
"@types/express": "^5.0.6",
|
|
68
|
+
"@types/node": "^22.19.7",
|
|
69
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
70
|
+
"@vitest/ui": "^4.0.17",
|
|
68
71
|
"husky": "^9.1.7",
|
|
69
|
-
"semantic-release": "^
|
|
70
|
-
"tsx": "^4.
|
|
72
|
+
"semantic-release": "^25.0.2",
|
|
73
|
+
"tsx": "^4.21.0",
|
|
71
74
|
"typescript": "^5.7.2",
|
|
72
|
-
"vitest": "^
|
|
75
|
+
"vitest": "^4.0.17"
|
|
73
76
|
}
|
|
74
77
|
}
|
|
@@ -2,21 +2,22 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { CohereEmbeddings } from "./cohere.js";
|
|
3
3
|
import { CohereClient } from "cohere-ai";
|
|
4
4
|
|
|
5
|
+
const mockClient = {
|
|
6
|
+
embed: vi.fn().mockResolvedValue({ embeddings: [[]] }),
|
|
7
|
+
};
|
|
8
|
+
|
|
5
9
|
vi.mock("cohere-ai", () => ({
|
|
6
|
-
CohereClient: vi.fn()
|
|
10
|
+
CohereClient: vi.fn().mockImplementation(function () {
|
|
11
|
+
return mockClient;
|
|
12
|
+
}),
|
|
7
13
|
}));
|
|
8
14
|
|
|
9
15
|
describe("CohereEmbeddings", () => {
|
|
10
16
|
let embeddings: CohereEmbeddings;
|
|
11
|
-
let mockClient: any;
|
|
12
17
|
|
|
13
18
|
beforeEach(() => {
|
|
14
|
-
mockClient
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
vi.mocked(CohereClient).mockImplementation(() => mockClient as any);
|
|
19
|
-
|
|
19
|
+
mockClient.embed.mockReset().mockResolvedValue({ embeddings: [[]] });
|
|
20
|
+
vi.mocked(CohereClient).mockClear();
|
|
20
21
|
embeddings = new CohereEmbeddings("test-api-key");
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -2,23 +2,26 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { OpenAIEmbeddings } from "./openai.js";
|
|
3
3
|
import OpenAI from "openai";
|
|
4
4
|
|
|
5
|
+
const mockOpenAI = {
|
|
6
|
+
embeddings: {
|
|
7
|
+
create: vi.fn().mockResolvedValue({ data: [{ embedding: [] }] }),
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
5
11
|
vi.mock("openai", () => ({
|
|
6
|
-
default: vi.fn()
|
|
12
|
+
default: vi.fn().mockImplementation(function () {
|
|
13
|
+
return mockOpenAI;
|
|
14
|
+
}),
|
|
7
15
|
}));
|
|
8
16
|
|
|
9
17
|
describe("OpenAIEmbeddings", () => {
|
|
10
18
|
let embeddings: OpenAIEmbeddings;
|
|
11
|
-
let mockOpenAI: any;
|
|
12
19
|
|
|
13
20
|
beforeEach(() => {
|
|
14
|
-
mockOpenAI
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
vi.mocked(OpenAI).mockImplementation(() => mockOpenAI as any);
|
|
21
|
-
|
|
21
|
+
mockOpenAI.embeddings.create
|
|
22
|
+
.mockReset()
|
|
23
|
+
.mockResolvedValue({ data: [{ embedding: [] }] });
|
|
24
|
+
vi.mocked(OpenAI).mockClear();
|
|
22
25
|
embeddings = new OpenAIEmbeddings("test-api-key");
|
|
23
26
|
});
|
|
24
27
|
|
package/src/index.ts
CHANGED
|
@@ -29,27 +29,37 @@ import { CodeIndexer } from "./code/indexer.js";
|
|
|
29
29
|
import type { CodeConfig } from "./code/types.js";
|
|
30
30
|
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
31
31
|
import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
getPrompt,
|
|
34
|
+
listPrompts,
|
|
35
|
+
loadPromptsConfig,
|
|
36
|
+
type PromptsConfig,
|
|
37
|
+
} from "./prompts/index.js";
|
|
33
38
|
import { renderTemplate, validateArguments } from "./prompts/template.js";
|
|
34
39
|
import { QdrantManager } from "./qdrant/client.js";
|
|
35
40
|
|
|
36
41
|
// Read package.json for version
|
|
37
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
const pkg = JSON.parse(
|
|
43
|
+
const pkg = JSON.parse(
|
|
44
|
+
readFileSync(join(__dirname, "../package.json"), "utf-8"),
|
|
45
|
+
);
|
|
39
46
|
|
|
40
47
|
// Validate environment variables
|
|
41
48
|
const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
|
|
42
49
|
const QDRANT_API_KEY = process.env.QDRANT_API_KEY;
|
|
43
|
-
const EMBEDDING_PROVIDER = (
|
|
50
|
+
const EMBEDDING_PROVIDER = (
|
|
51
|
+
process.env.EMBEDDING_PROVIDER || "ollama"
|
|
52
|
+
).toLowerCase();
|
|
44
53
|
const TRANSPORT_MODE = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
|
|
45
54
|
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "3000", 10);
|
|
46
|
-
const PROMPTS_CONFIG_FILE =
|
|
55
|
+
const PROMPTS_CONFIG_FILE =
|
|
56
|
+
process.env.PROMPTS_CONFIG_FILE || join(__dirname, "../prompts.json");
|
|
47
57
|
|
|
48
58
|
// Validate HTTP_PORT when HTTP mode is selected
|
|
49
59
|
if (TRANSPORT_MODE === "http") {
|
|
50
60
|
if (Number.isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
|
|
51
61
|
console.error(
|
|
52
|
-
`Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535
|
|
62
|
+
`Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535.`,
|
|
53
63
|
);
|
|
54
64
|
process.exit(1);
|
|
55
65
|
}
|
|
@@ -75,13 +85,15 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
75
85
|
break;
|
|
76
86
|
default:
|
|
77
87
|
console.error(
|
|
78
|
-
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama
|
|
88
|
+
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`,
|
|
79
89
|
);
|
|
80
90
|
process.exit(1);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
if (!apiKey) {
|
|
84
|
-
console.error(
|
|
94
|
+
console.error(
|
|
95
|
+
`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
|
|
96
|
+
);
|
|
85
97
|
process.exit(1);
|
|
86
98
|
}
|
|
87
99
|
}
|
|
@@ -90,7 +102,8 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
90
102
|
async function checkOllamaAvailability() {
|
|
91
103
|
if (EMBEDDING_PROVIDER === "ollama") {
|
|
92
104
|
const baseUrl = process.env.EMBEDDING_BASE_URL || "http://localhost:11434";
|
|
93
|
-
const isLocalhost =
|
|
105
|
+
const isLocalhost =
|
|
106
|
+
baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
|
|
94
107
|
|
|
95
108
|
try {
|
|
96
109
|
const response = await fetch(`${baseUrl}/api/version`);
|
|
@@ -103,7 +116,7 @@ async function checkOllamaAvailability() {
|
|
|
103
116
|
const { models } = await tagsResponse.json();
|
|
104
117
|
const modelName = process.env.EMBEDDING_MODEL || "nomic-embed-text";
|
|
105
118
|
const modelExists = models.some(
|
|
106
|
-
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
|
|
119
|
+
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`),
|
|
107
120
|
);
|
|
108
121
|
|
|
109
122
|
if (!modelExists) {
|
|
@@ -112,6 +125,7 @@ async function checkOllamaAvailability() {
|
|
|
112
125
|
if (isLocalhost) {
|
|
113
126
|
errorMessage +=
|
|
114
127
|
`Pull it with:\n` +
|
|
128
|
+
` - Using Podman: podman exec ollama ollama pull ${modelName}\n` +
|
|
115
129
|
` - Using Docker: docker exec ollama ollama pull ${modelName}\n` +
|
|
116
130
|
` - Or locally: ollama pull ${modelName}`;
|
|
117
131
|
} else {
|
|
@@ -133,6 +147,7 @@ async function checkOllamaAvailability() {
|
|
|
133
147
|
if (isLocalhost) {
|
|
134
148
|
helpText =
|
|
135
149
|
`Please start Ollama:\n` +
|
|
150
|
+
` - Using Podman: podman compose up -d\n` +
|
|
136
151
|
` - Using Docker: docker compose up -d\n` +
|
|
137
152
|
` - Or install locally: curl -fsSL https://ollama.ai/install.sh | sh\n` +
|
|
138
153
|
`\nThen pull the embedding model:\n` +
|
|
@@ -157,13 +172,25 @@ const embeddings = EmbeddingProviderFactory.createFromEnv();
|
|
|
157
172
|
|
|
158
173
|
// Initialize code indexer
|
|
159
174
|
const codeConfig: CodeConfig = {
|
|
160
|
-
chunkSize: parseInt(
|
|
161
|
-
|
|
175
|
+
chunkSize: parseInt(
|
|
176
|
+
process.env.CODE_CHUNK_SIZE || String(DEFAULT_CHUNK_SIZE),
|
|
177
|
+
10,
|
|
178
|
+
),
|
|
179
|
+
chunkOverlap: parseInt(
|
|
180
|
+
process.env.CODE_CHUNK_OVERLAP || String(DEFAULT_CHUNK_OVERLAP),
|
|
181
|
+
10,
|
|
182
|
+
),
|
|
162
183
|
enableASTChunking: process.env.CODE_ENABLE_AST !== "false",
|
|
163
184
|
supportedExtensions: DEFAULT_CODE_EXTENSIONS,
|
|
164
185
|
ignorePatterns: DEFAULT_IGNORE_PATTERNS,
|
|
165
|
-
batchSize: parseInt(
|
|
166
|
-
|
|
186
|
+
batchSize: parseInt(
|
|
187
|
+
process.env.CODE_BATCH_SIZE || String(DEFAULT_BATCH_SIZE),
|
|
188
|
+
10,
|
|
189
|
+
),
|
|
190
|
+
defaultSearchLimit: parseInt(
|
|
191
|
+
process.env.CODE_SEARCH_LIMIT || String(DEFAULT_SEARCH_LIMIT),
|
|
192
|
+
10,
|
|
193
|
+
),
|
|
167
194
|
enableHybridSearch: process.env.CODE_ENABLE_HYBRID === "true",
|
|
168
195
|
};
|
|
169
196
|
|
|
@@ -174,9 +201,14 @@ let promptsConfig: PromptsConfig | null = null;
|
|
|
174
201
|
if (existsSync(PROMPTS_CONFIG_FILE)) {
|
|
175
202
|
try {
|
|
176
203
|
promptsConfig = loadPromptsConfig(PROMPTS_CONFIG_FILE);
|
|
177
|
-
console.error(
|
|
204
|
+
console.error(
|
|
205
|
+
`Loaded ${promptsConfig.prompts.length} prompts from ${PROMPTS_CONFIG_FILE}`,
|
|
206
|
+
);
|
|
178
207
|
} catch (error) {
|
|
179
|
-
console.error(
|
|
208
|
+
console.error(
|
|
209
|
+
`Failed to load prompts configuration from ${PROMPTS_CONFIG_FILE}:`,
|
|
210
|
+
error,
|
|
211
|
+
);
|
|
180
212
|
process.exit(1);
|
|
181
213
|
}
|
|
182
214
|
}
|
|
@@ -205,7 +237,7 @@ function createServer() {
|
|
|
205
237
|
},
|
|
206
238
|
{
|
|
207
239
|
capabilities,
|
|
208
|
-
}
|
|
240
|
+
},
|
|
209
241
|
);
|
|
210
242
|
}
|
|
211
243
|
|
|
@@ -236,7 +268,8 @@ function registerHandlers(server: Server) {
|
|
|
236
268
|
},
|
|
237
269
|
enableHybrid: {
|
|
238
270
|
type: "boolean",
|
|
239
|
-
description:
|
|
271
|
+
description:
|
|
272
|
+
"Enable hybrid search with sparse vectors (default: false)",
|
|
240
273
|
},
|
|
241
274
|
},
|
|
242
275
|
required: ["name"],
|
|
@@ -269,7 +302,8 @@ function registerHandlers(server: Server) {
|
|
|
269
302
|
},
|
|
270
303
|
metadata: {
|
|
271
304
|
type: "object",
|
|
272
|
-
description:
|
|
305
|
+
description:
|
|
306
|
+
"Optional metadata to store with the document",
|
|
273
307
|
},
|
|
274
308
|
},
|
|
275
309
|
required: ["id", "text"],
|
|
@@ -345,7 +379,8 @@ function registerHandlers(server: Server) {
|
|
|
345
379
|
},
|
|
346
380
|
{
|
|
347
381
|
name: "delete_documents",
|
|
348
|
-
description:
|
|
382
|
+
description:
|
|
383
|
+
"Delete specific documents from a collection by their IDs.",
|
|
349
384
|
inputSchema: {
|
|
350
385
|
type: "object",
|
|
351
386
|
properties: {
|
|
@@ -400,21 +435,25 @@ function registerHandlers(server: Server) {
|
|
|
400
435
|
properties: {
|
|
401
436
|
path: {
|
|
402
437
|
type: "string",
|
|
403
|
-
description:
|
|
438
|
+
description:
|
|
439
|
+
"Absolute or relative path to codebase root directory",
|
|
404
440
|
},
|
|
405
441
|
forceReindex: {
|
|
406
442
|
type: "boolean",
|
|
407
|
-
description:
|
|
443
|
+
description:
|
|
444
|
+
"Force full re-index even if already indexed (default: false)",
|
|
408
445
|
},
|
|
409
446
|
extensions: {
|
|
410
447
|
type: "array",
|
|
411
448
|
items: { type: "string" },
|
|
412
|
-
description:
|
|
449
|
+
description:
|
|
450
|
+
"Custom file extensions to index (e.g., ['.proto', '.graphql'])",
|
|
413
451
|
},
|
|
414
452
|
ignorePatterns: {
|
|
415
453
|
type: "array",
|
|
416
454
|
items: { type: "string" },
|
|
417
|
-
description:
|
|
455
|
+
description:
|
|
456
|
+
"Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
|
|
418
457
|
},
|
|
419
458
|
},
|
|
420
459
|
required: ["path"],
|
|
@@ -433,7 +472,8 @@ function registerHandlers(server: Server) {
|
|
|
433
472
|
},
|
|
434
473
|
query: {
|
|
435
474
|
type: "string",
|
|
436
|
-
description:
|
|
475
|
+
description:
|
|
476
|
+
"Natural language search query (e.g., 'authentication logic')",
|
|
437
477
|
},
|
|
438
478
|
limit: {
|
|
439
479
|
type: "number",
|
|
@@ -446,7 +486,8 @@ function registerHandlers(server: Server) {
|
|
|
446
486
|
},
|
|
447
487
|
pathPattern: {
|
|
448
488
|
type: "string",
|
|
449
|
-
description:
|
|
489
|
+
description:
|
|
490
|
+
"Filter by path glob pattern (e.g., 'src/services/**')",
|
|
450
491
|
},
|
|
451
492
|
},
|
|
452
493
|
required: ["path", "query"],
|
|
@@ -507,9 +548,15 @@ function registerHandlers(server: Server) {
|
|
|
507
548
|
try {
|
|
508
549
|
switch (name) {
|
|
509
550
|
case "create_collection": {
|
|
510
|
-
const { name, distance, enableHybrid } =
|
|
551
|
+
const { name, distance, enableHybrid } =
|
|
552
|
+
CreateCollectionSchema.parse(args);
|
|
511
553
|
const vectorSize = embeddings.getDimensions();
|
|
512
|
-
await qdrant.createCollection(
|
|
554
|
+
await qdrant.createCollection(
|
|
555
|
+
name,
|
|
556
|
+
vectorSize,
|
|
557
|
+
distance,
|
|
558
|
+
enableHybrid || false,
|
|
559
|
+
);
|
|
513
560
|
|
|
514
561
|
let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
|
|
515
562
|
if (enableHybrid) {
|
|
@@ -590,7 +637,8 @@ function registerHandlers(server: Server) {
|
|
|
590
637
|
}
|
|
591
638
|
|
|
592
639
|
case "semantic_search": {
|
|
593
|
-
const { collection, query, limit, filter } =
|
|
640
|
+
const { collection, query, limit, filter } =
|
|
641
|
+
SemanticSearchSchema.parse(args);
|
|
594
642
|
|
|
595
643
|
// Check if collection exists
|
|
596
644
|
const exists = await qdrant.collectionExists(collection);
|
|
@@ -610,7 +658,12 @@ function registerHandlers(server: Server) {
|
|
|
610
658
|
const { embedding } = await embeddings.embed(query);
|
|
611
659
|
|
|
612
660
|
// Search
|
|
613
|
-
const results = await qdrant.search(
|
|
661
|
+
const results = await qdrant.search(
|
|
662
|
+
collection,
|
|
663
|
+
embedding,
|
|
664
|
+
limit || 5,
|
|
665
|
+
filter,
|
|
666
|
+
);
|
|
614
667
|
|
|
615
668
|
return {
|
|
616
669
|
content: [
|
|
@@ -674,7 +727,8 @@ function registerHandlers(server: Server) {
|
|
|
674
727
|
}
|
|
675
728
|
|
|
676
729
|
case "hybrid_search": {
|
|
677
|
-
const { collection, query, limit, filter } =
|
|
730
|
+
const { collection, query, limit, filter } =
|
|
731
|
+
HybridSearchSchema.parse(args);
|
|
678
732
|
|
|
679
733
|
// Check if collection exists
|
|
680
734
|
const exists = await qdrant.collectionExists(collection);
|
|
@@ -717,7 +771,7 @@ function registerHandlers(server: Server) {
|
|
|
717
771
|
embedding,
|
|
718
772
|
sparseVector,
|
|
719
773
|
limit || 5,
|
|
720
|
-
filter
|
|
774
|
+
filter,
|
|
721
775
|
);
|
|
722
776
|
|
|
723
777
|
return {
|
|
@@ -746,8 +800,10 @@ function registerHandlers(server: Server) {
|
|
|
746
800
|
{ forceReindex, extensions, ignorePatterns },
|
|
747
801
|
(progress) => {
|
|
748
802
|
// Progress callback - could send progress updates via SSE in future
|
|
749
|
-
console.error(
|
|
750
|
-
|
|
803
|
+
console.error(
|
|
804
|
+
`[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
|
|
805
|
+
);
|
|
806
|
+
},
|
|
751
807
|
);
|
|
752
808
|
|
|
753
809
|
let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
|
|
@@ -778,7 +834,8 @@ function registerHandlers(server: Server) {
|
|
|
778
834
|
pathPattern: z.string().optional(),
|
|
779
835
|
});
|
|
780
836
|
|
|
781
|
-
const { path, query, limit, fileTypes, pathPattern } =
|
|
837
|
+
const { path, query, limit, fileTypes, pathPattern } =
|
|
838
|
+
SearchCodeSchema.parse(args);
|
|
782
839
|
|
|
783
840
|
const results = await codeIndexer.searchCode(path, query, {
|
|
784
841
|
limit,
|
|
@@ -804,7 +861,7 @@ function registerHandlers(server: Server) {
|
|
|
804
861
|
`\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
|
|
805
862
|
`File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
|
|
806
863
|
`Language: ${r.language}\n\n` +
|
|
807
|
-
`${r.content}\n
|
|
864
|
+
`${r.content}\n`,
|
|
808
865
|
)
|
|
809
866
|
.join("\n");
|
|
810
867
|
|
|
@@ -855,7 +912,9 @@ function registerHandlers(server: Server) {
|
|
|
855
912
|
const { path } = ReindexChangesSchema.parse(args);
|
|
856
913
|
|
|
857
914
|
const stats = await codeIndexer.reindexChanges(path, (progress) => {
|
|
858
|
-
console.error(
|
|
915
|
+
console.error(
|
|
916
|
+
`[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
|
|
917
|
+
);
|
|
859
918
|
});
|
|
860
919
|
|
|
861
920
|
let message = `Incremental re-index complete:\n`;
|
|
@@ -865,7 +924,11 @@ function registerHandlers(server: Server) {
|
|
|
865
924
|
message += `- Chunks added: ${stats.chunksAdded}\n`;
|
|
866
925
|
message += `- Duration: ${(stats.durationMs / 1000).toFixed(1)}s`;
|
|
867
926
|
|
|
868
|
-
if (
|
|
927
|
+
if (
|
|
928
|
+
stats.filesAdded === 0 &&
|
|
929
|
+
stats.filesModified === 0 &&
|
|
930
|
+
stats.filesDeleted === 0
|
|
931
|
+
) {
|
|
869
932
|
message = `No changes detected. Codebase is up to date.`;
|
|
870
933
|
}
|
|
871
934
|
|
|
@@ -910,7 +973,8 @@ function registerHandlers(server: Server) {
|
|
|
910
973
|
}
|
|
911
974
|
} catch (error: any) {
|
|
912
975
|
// Enhanced error details for debugging
|
|
913
|
-
const errorDetails =
|
|
976
|
+
const errorDetails =
|
|
977
|
+
error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
914
978
|
|
|
915
979
|
console.error("Tool execution error:", {
|
|
916
980
|
tool: name,
|
|
@@ -1028,7 +1092,11 @@ function registerHandlers(server: Server) {
|
|
|
1028
1092
|
validateArguments(args || {}, prompt.arguments);
|
|
1029
1093
|
|
|
1030
1094
|
// Render template
|
|
1031
|
-
const rendered = renderTemplate(
|
|
1095
|
+
const rendered = renderTemplate(
|
|
1096
|
+
prompt.template,
|
|
1097
|
+
args || {},
|
|
1098
|
+
prompt.arguments,
|
|
1099
|
+
);
|
|
1032
1100
|
|
|
1033
1101
|
return {
|
|
1034
1102
|
messages: [
|
|
@@ -1043,7 +1111,7 @@ function registerHandlers(server: Server) {
|
|
|
1043
1111
|
};
|
|
1044
1112
|
} catch (error) {
|
|
1045
1113
|
throw new Error(
|
|
1046
|
-
`Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}
|
|
1114
|
+
`Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}`,
|
|
1047
1115
|
);
|
|
1048
1116
|
}
|
|
1049
1117
|
});
|
|
@@ -1071,13 +1139,15 @@ const AddDocumentsSchema = z.object({
|
|
|
1071
1139
|
documents: z
|
|
1072
1140
|
.array(
|
|
1073
1141
|
z.object({
|
|
1074
|
-
id: z
|
|
1142
|
+
id: z
|
|
1143
|
+
.union([z.string(), z.number()])
|
|
1144
|
+
.describe("Unique identifier for the document"),
|
|
1075
1145
|
text: z.string().describe("Text content to embed and store"),
|
|
1076
1146
|
metadata: z
|
|
1077
1147
|
.record(z.any())
|
|
1078
1148
|
.optional()
|
|
1079
1149
|
.describe("Optional metadata to store with the document"),
|
|
1080
|
-
})
|
|
1150
|
+
}),
|
|
1081
1151
|
)
|
|
1082
1152
|
.describe("Array of documents to add"),
|
|
1083
1153
|
});
|
|
@@ -1085,7 +1155,10 @@ const AddDocumentsSchema = z.object({
|
|
|
1085
1155
|
const SemanticSearchSchema = z.object({
|
|
1086
1156
|
collection: z.string().describe("Name of the collection to search"),
|
|
1087
1157
|
query: z.string().describe("Search query text"),
|
|
1088
|
-
limit: z
|
|
1158
|
+
limit: z
|
|
1159
|
+
.number()
|
|
1160
|
+
.optional()
|
|
1161
|
+
.describe("Maximum number of results (default: 5)"),
|
|
1089
1162
|
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
1090
1163
|
});
|
|
1091
1164
|
|
|
@@ -1099,13 +1172,18 @@ const GetCollectionInfoSchema = z.object({
|
|
|
1099
1172
|
|
|
1100
1173
|
const DeleteDocumentsSchema = z.object({
|
|
1101
1174
|
collection: z.string().describe("Name of the collection"),
|
|
1102
|
-
ids: z
|
|
1175
|
+
ids: z
|
|
1176
|
+
.array(z.union([z.string(), z.number()]))
|
|
1177
|
+
.describe("Array of document IDs to delete"),
|
|
1103
1178
|
});
|
|
1104
1179
|
|
|
1105
1180
|
const HybridSearchSchema = z.object({
|
|
1106
1181
|
collection: z.string().describe("Name of the collection to search"),
|
|
1107
1182
|
query: z.string().describe("Search query text"),
|
|
1108
|
-
limit: z
|
|
1183
|
+
limit: z
|
|
1184
|
+
.number()
|
|
1185
|
+
.optional()
|
|
1186
|
+
.describe("Maximum number of results (default: 5)"),
|
|
1109
1187
|
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
1110
1188
|
});
|
|
1111
1189
|
|
|
@@ -1122,9 +1200,20 @@ const RATE_LIMIT_MAX_REQUESTS = 100; // Max requests per window
|
|
|
1122
1200
|
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
1123
1201
|
const RATE_LIMIT_MAX_CONCURRENT = 10; // Max concurrent requests per IP
|
|
1124
1202
|
const RATE_LIMITER_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
1125
|
-
const REQUEST_TIMEOUT_MS =
|
|
1203
|
+
const REQUEST_TIMEOUT_MS = parseInt(
|
|
1204
|
+
process.env.HTTP_REQUEST_TIMEOUT_MS || "300000",
|
|
1205
|
+
10,
|
|
1206
|
+
);
|
|
1126
1207
|
const SHUTDOWN_GRACE_PERIOD_MS = 10 * 1000; // 10 seconds
|
|
1127
1208
|
|
|
1209
|
+
// Validate REQUEST_TIMEOUT_MS
|
|
1210
|
+
if (Number.isNaN(REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS <= 0) {
|
|
1211
|
+
console.error(
|
|
1212
|
+
`Error: Invalid HTTP_REQUEST_TIMEOUT_MS "${process.env.HTTP_REQUEST_TIMEOUT_MS}". Must be a positive integer.`,
|
|
1213
|
+
);
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1128
1217
|
// Start server with HTTP transport
|
|
1129
1218
|
async function startHttpServer() {
|
|
1130
1219
|
await checkOllamaAvailability();
|
|
@@ -1148,7 +1237,7 @@ async function startHttpServer() {
|
|
|
1148
1237
|
res: express.Response,
|
|
1149
1238
|
code: number,
|
|
1150
1239
|
message: string,
|
|
1151
|
-
httpStatus: number = 500
|
|
1240
|
+
httpStatus: number = 500,
|
|
1152
1241
|
) => {
|
|
1153
1242
|
if (!res.headersSent) {
|
|
1154
1243
|
res.status(httpStatus).json({
|
|
@@ -1187,7 +1276,7 @@ async function startHttpServer() {
|
|
|
1187
1276
|
const rateLimitMiddleware = async (
|
|
1188
1277
|
req: express.Request,
|
|
1189
1278
|
res: express.Response,
|
|
1190
|
-
next: express.NextFunction
|
|
1279
|
+
next: express.NextFunction,
|
|
1191
1280
|
) => {
|
|
1192
1281
|
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
|
1193
1282
|
|
|
@@ -1281,7 +1370,9 @@ async function startHttpServer() {
|
|
|
1281
1370
|
|
|
1282
1371
|
const httpServer = app
|
|
1283
1372
|
.listen(HTTP_PORT, () => {
|
|
1284
|
-
console.error(
|
|
1373
|
+
console.error(
|
|
1374
|
+
`Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`,
|
|
1375
|
+
);
|
|
1285
1376
|
})
|
|
1286
1377
|
.on("error", (error) => {
|
|
1287
1378
|
console.error("HTTP server error:", error);
|
|
@@ -1295,7 +1386,9 @@ async function startHttpServer() {
|
|
|
1295
1386
|
if (isShuttingDown) return;
|
|
1296
1387
|
isShuttingDown = true;
|
|
1297
1388
|
|
|
1298
|
-
console.error(
|
|
1389
|
+
console.error(
|
|
1390
|
+
"Shutdown signal received, closing HTTP server gracefully...",
|
|
1391
|
+
);
|
|
1299
1392
|
|
|
1300
1393
|
// Clear the cleanup interval to allow graceful shutdown
|
|
1301
1394
|
clearInterval(cleanupIntervalId);
|
|
@@ -1325,7 +1418,7 @@ async function main() {
|
|
|
1325
1418
|
await startStdioServer();
|
|
1326
1419
|
} else {
|
|
1327
1420
|
console.error(
|
|
1328
|
-
`Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http
|
|
1421
|
+
`Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`,
|
|
1329
1422
|
);
|
|
1330
1423
|
process.exit(1);
|
|
1331
1424
|
}
|