@mhalder/qdrant-mcp-server 1.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/.env.example +92 -0
- package/.github/workflows/ci.yml +61 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/release.yml +52 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.releaserc.json +59 -0
- package/.yamlfmt +4 -0
- package/CHANGELOG.md +73 -0
- package/CONTRIBUTING.md +176 -0
- package/LICENSE +21 -0
- package/README.md +714 -0
- package/build/embeddings/base.d.ts +23 -0
- package/build/embeddings/base.d.ts.map +1 -0
- package/build/embeddings/base.js +2 -0
- package/build/embeddings/base.js.map +1 -0
- package/build/embeddings/cohere.d.ts +17 -0
- package/build/embeddings/cohere.d.ts.map +1 -0
- package/build/embeddings/cohere.js +102 -0
- package/build/embeddings/cohere.js.map +1 -0
- package/build/embeddings/cohere.test.d.ts +2 -0
- package/build/embeddings/cohere.test.d.ts.map +1 -0
- package/build/embeddings/cohere.test.js +279 -0
- package/build/embeddings/cohere.test.js.map +1 -0
- package/build/embeddings/factory.d.ts +10 -0
- package/build/embeddings/factory.d.ts.map +1 -0
- package/build/embeddings/factory.js +98 -0
- package/build/embeddings/factory.js.map +1 -0
- package/build/embeddings/factory.test.d.ts +2 -0
- package/build/embeddings/factory.test.d.ts.map +1 -0
- package/build/embeddings/factory.test.js +329 -0
- package/build/embeddings/factory.test.js.map +1 -0
- package/build/embeddings/ollama.d.ts +18 -0
- package/build/embeddings/ollama.d.ts.map +1 -0
- package/build/embeddings/ollama.js +135 -0
- package/build/embeddings/ollama.js.map +1 -0
- package/build/embeddings/ollama.test.d.ts +2 -0
- package/build/embeddings/ollama.test.d.ts.map +1 -0
- package/build/embeddings/ollama.test.js +399 -0
- package/build/embeddings/ollama.test.js.map +1 -0
- package/build/embeddings/openai.d.ts +16 -0
- package/build/embeddings/openai.d.ts.map +1 -0
- package/build/embeddings/openai.js +108 -0
- package/build/embeddings/openai.js.map +1 -0
- package/build/embeddings/openai.test.d.ts +2 -0
- package/build/embeddings/openai.test.d.ts.map +1 -0
- package/build/embeddings/openai.test.js +283 -0
- package/build/embeddings/openai.test.js.map +1 -0
- package/build/embeddings/voyage.d.ts +19 -0
- package/build/embeddings/voyage.d.ts.map +1 -0
- package/build/embeddings/voyage.js +113 -0
- package/build/embeddings/voyage.js.map +1 -0
- package/build/embeddings/voyage.test.d.ts +2 -0
- package/build/embeddings/voyage.test.d.ts.map +1 -0
- package/build/embeddings/voyage.test.js +371 -0
- package/build/embeddings/voyage.test.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +534 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +2 -0
- package/build/index.test.d.ts.map +1 -0
- package/build/index.test.js +241 -0
- package/build/index.test.js.map +1 -0
- package/build/qdrant/client.d.ts +37 -0
- package/build/qdrant/client.d.ts.map +1 -0
- package/build/qdrant/client.js +142 -0
- package/build/qdrant/client.js.map +1 -0
- package/build/qdrant/client.test.d.ts +2 -0
- package/build/qdrant/client.test.d.ts.map +1 -0
- package/build/qdrant/client.test.js +340 -0
- package/build/qdrant/client.test.js.map +1 -0
- package/commitlint.config.js +25 -0
- package/docker-compose.yml +22 -0
- package/docs/test_report.md +259 -0
- package/examples/README.md +315 -0
- package/examples/basic/README.md +111 -0
- package/examples/filters/README.md +262 -0
- package/examples/knowledge-base/README.md +207 -0
- package/examples/rate-limiting/README.md +376 -0
- package/package.json +59 -0
- package/scripts/verify-providers.js +238 -0
- package/src/embeddings/base.ts +25 -0
- package/src/embeddings/cohere.test.ts +408 -0
- package/src/embeddings/cohere.ts +152 -0
- package/src/embeddings/factory.test.ts +453 -0
- package/src/embeddings/factory.ts +163 -0
- package/src/embeddings/ollama.test.ts +543 -0
- package/src/embeddings/ollama.ts +196 -0
- package/src/embeddings/openai.test.ts +402 -0
- package/src/embeddings/openai.ts +158 -0
- package/src/embeddings/voyage.test.ts +520 -0
- package/src/embeddings/voyage.ts +168 -0
- package/src/index.test.ts +304 -0
- package/src/index.ts +614 -0
- package/src/qdrant/client.test.ts +456 -0
- package/src/qdrant/client.ts +195 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +37 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import Bottleneck from "bottleneck";
|
|
2
|
+
import { EmbeddingProvider, EmbeddingResult, RateLimitConfig } from "./base.js";
|
|
3
|
+
|
|
4
|
+
interface VoyageError {
|
|
5
|
+
status?: number;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface VoyageEmbedResponse {
|
|
10
|
+
data: Array<{ embedding: number[] }>;
|
|
11
|
+
model: string;
|
|
12
|
+
usage: {
|
|
13
|
+
total_tokens: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class VoyageEmbeddings implements EmbeddingProvider {
|
|
18
|
+
private apiKey: string;
|
|
19
|
+
private model: string;
|
|
20
|
+
private dimensions: number;
|
|
21
|
+
private limiter: Bottleneck;
|
|
22
|
+
private retryAttempts: number;
|
|
23
|
+
private retryDelayMs: number;
|
|
24
|
+
private baseUrl: string;
|
|
25
|
+
private inputType?: "query" | "document";
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
apiKey: string,
|
|
29
|
+
model: string = "voyage-2",
|
|
30
|
+
dimensions?: number,
|
|
31
|
+
rateLimitConfig?: RateLimitConfig,
|
|
32
|
+
baseUrl: string = "https://api.voyageai.com/v1",
|
|
33
|
+
inputType?: "query" | "document",
|
|
34
|
+
) {
|
|
35
|
+
this.apiKey = apiKey;
|
|
36
|
+
this.model = model;
|
|
37
|
+
this.baseUrl = baseUrl;
|
|
38
|
+
this.inputType = inputType;
|
|
39
|
+
|
|
40
|
+
// Default dimensions for different models
|
|
41
|
+
const defaultDimensions: Record<string, number> = {
|
|
42
|
+
"voyage-2": 1024,
|
|
43
|
+
"voyage-large-2": 1536,
|
|
44
|
+
"voyage-code-2": 1536,
|
|
45
|
+
"voyage-lite-02-instruct": 1024,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.dimensions = dimensions || defaultDimensions[model] || 1024;
|
|
49
|
+
|
|
50
|
+
// Rate limiting configuration
|
|
51
|
+
const maxRequestsPerMinute = rateLimitConfig?.maxRequestsPerMinute || 300;
|
|
52
|
+
this.retryAttempts = rateLimitConfig?.retryAttempts || 3;
|
|
53
|
+
this.retryDelayMs = rateLimitConfig?.retryDelayMs || 1000;
|
|
54
|
+
|
|
55
|
+
this.limiter = new Bottleneck({
|
|
56
|
+
reservoir: maxRequestsPerMinute,
|
|
57
|
+
reservoirRefreshAmount: maxRequestsPerMinute,
|
|
58
|
+
reservoirRefreshInterval: 60 * 1000,
|
|
59
|
+
maxConcurrent: 5,
|
|
60
|
+
minTime: Math.floor((60 * 1000) / maxRequestsPerMinute),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async retryWithBackoff<T>(
|
|
65
|
+
fn: () => Promise<T>,
|
|
66
|
+
attempt: number = 0,
|
|
67
|
+
): Promise<T> {
|
|
68
|
+
try {
|
|
69
|
+
return await fn();
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
const apiError = error as VoyageError;
|
|
72
|
+
const isRateLimitError =
|
|
73
|
+
apiError?.status === 429 ||
|
|
74
|
+
apiError?.message?.toLowerCase().includes("rate limit");
|
|
75
|
+
|
|
76
|
+
if (isRateLimitError && attempt < this.retryAttempts) {
|
|
77
|
+
const delayMs = this.retryDelayMs * Math.pow(2, attempt);
|
|
78
|
+
const waitTimeSeconds = (delayMs / 1000).toFixed(1);
|
|
79
|
+
console.error(
|
|
80
|
+
`Rate limit reached. Retrying in ${waitTimeSeconds}s (attempt ${attempt + 1}/${this.retryAttempts})...`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
84
|
+
return this.retryWithBackoff(fn, attempt + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isRateLimitError) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Voyage AI API rate limit exceeded after ${this.retryAttempts} retry attempts. Please try again later or reduce request frequency.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async callApi(texts: string[]): Promise<VoyageEmbedResponse> {
|
|
98
|
+
const body: any = {
|
|
99
|
+
input: texts,
|
|
100
|
+
model: this.model,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (this.inputType) {
|
|
104
|
+
body.input_type = this.inputType;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error: VoyageError = {
|
|
118
|
+
status: response.status,
|
|
119
|
+
message: await response.text(),
|
|
120
|
+
};
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async embed(text: string): Promise<EmbeddingResult> {
|
|
128
|
+
return this.limiter.schedule(() =>
|
|
129
|
+
this.retryWithBackoff(async () => {
|
|
130
|
+
const response = await this.callApi([text]);
|
|
131
|
+
|
|
132
|
+
if (!response.data || response.data.length === 0) {
|
|
133
|
+
throw new Error("No embedding returned from Voyage AI API");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
embedding: response.data[0].embedding,
|
|
138
|
+
dimensions: this.dimensions,
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
|
|
145
|
+
return this.limiter.schedule(() =>
|
|
146
|
+
this.retryWithBackoff(async () => {
|
|
147
|
+
const response = await this.callApi(texts);
|
|
148
|
+
|
|
149
|
+
if (!response.data) {
|
|
150
|
+
throw new Error("No embeddings returned from Voyage AI API");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return response.data.map((item) => ({
|
|
154
|
+
embedding: item.embedding,
|
|
155
|
+
dimensions: this.dimensions,
|
|
156
|
+
}));
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getDimensions(): number {
|
|
162
|
+
return this.dimensions;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getModel(): string {
|
|
166
|
+
return this.model;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('./qdrant/client.js');
|
|
4
|
+
vi.mock('./embeddings/openai.js');
|
|
5
|
+
|
|
6
|
+
describe('MCP Server Tool Schemas', () => {
|
|
7
|
+
describe('CreateCollectionSchema', () => {
|
|
8
|
+
it('should validate correct collection creation input', async () => {
|
|
9
|
+
const { z } = await import('zod');
|
|
10
|
+
|
|
11
|
+
const CreateCollectionSchema = z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
distance: z.enum(['Cosine', 'Euclid', 'Dot']).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const validInput = { name: 'test-collection' };
|
|
17
|
+
expect(() => CreateCollectionSchema.parse(validInput)).not.toThrow();
|
|
18
|
+
|
|
19
|
+
const validInputWithDistance = { name: 'test-collection', distance: 'Cosine' as const };
|
|
20
|
+
expect(() => CreateCollectionSchema.parse(validInputWithDistance)).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should reject invalid distance metric', async () => {
|
|
24
|
+
const { z } = await import('zod');
|
|
25
|
+
|
|
26
|
+
const CreateCollectionSchema = z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
distance: z.enum(['Cosine', 'Euclid', 'Dot']).optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const invalidInput = { name: 'test', distance: 'Invalid' };
|
|
32
|
+
expect(() => CreateCollectionSchema.parse(invalidInput)).toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should require name field', async () => {
|
|
36
|
+
const { z } = await import('zod');
|
|
37
|
+
|
|
38
|
+
const CreateCollectionSchema = z.object({
|
|
39
|
+
name: z.string(),
|
|
40
|
+
distance: z.enum(['Cosine', 'Euclid', 'Dot']).optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const invalidInput = { distance: 'Cosine' };
|
|
44
|
+
expect(() => CreateCollectionSchema.parse(invalidInput)).toThrow();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('AddDocumentsSchema', () => {
|
|
49
|
+
it('should validate correct document addition input', async () => {
|
|
50
|
+
const { z } = await import('zod');
|
|
51
|
+
|
|
52
|
+
const AddDocumentsSchema = z.object({
|
|
53
|
+
collection: z.string(),
|
|
54
|
+
documents: z.array(z.object({
|
|
55
|
+
id: z.union([z.string(), z.number()]),
|
|
56
|
+
text: z.string(),
|
|
57
|
+
metadata: z.record(z.any()).optional(),
|
|
58
|
+
})),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const validInput = {
|
|
62
|
+
collection: 'test-collection',
|
|
63
|
+
documents: [
|
|
64
|
+
{ id: 1, text: 'test document' },
|
|
65
|
+
{ id: 'doc-2', text: 'another document', metadata: { type: 'test' } },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
expect(() => AddDocumentsSchema.parse(validInput)).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should accept both string and number IDs', async () => {
|
|
73
|
+
const { z } = await import('zod');
|
|
74
|
+
|
|
75
|
+
const AddDocumentsSchema = z.object({
|
|
76
|
+
collection: z.string(),
|
|
77
|
+
documents: z.array(z.object({
|
|
78
|
+
id: z.union([z.string(), z.number()]),
|
|
79
|
+
text: z.string(),
|
|
80
|
+
metadata: z.record(z.any()).optional(),
|
|
81
|
+
})),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const stringIdInput = {
|
|
85
|
+
collection: 'test',
|
|
86
|
+
documents: [{ id: 'string-id', text: 'test' }],
|
|
87
|
+
};
|
|
88
|
+
expect(() => AddDocumentsSchema.parse(stringIdInput)).not.toThrow();
|
|
89
|
+
|
|
90
|
+
const numberIdInput = {
|
|
91
|
+
collection: 'test',
|
|
92
|
+
documents: [{ id: 123, text: 'test' }],
|
|
93
|
+
};
|
|
94
|
+
expect(() => AddDocumentsSchema.parse(numberIdInput)).not.toThrow();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should require text field in documents', async () => {
|
|
98
|
+
const { z } = await import('zod');
|
|
99
|
+
|
|
100
|
+
const AddDocumentsSchema = z.object({
|
|
101
|
+
collection: z.string(),
|
|
102
|
+
documents: z.array(z.object({
|
|
103
|
+
id: z.union([z.string(), z.number()]),
|
|
104
|
+
text: z.string(),
|
|
105
|
+
metadata: z.record(z.any()).optional(),
|
|
106
|
+
})),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const invalidInput = {
|
|
110
|
+
collection: 'test',
|
|
111
|
+
documents: [{ id: 1, metadata: {} }],
|
|
112
|
+
};
|
|
113
|
+
expect(() => AddDocumentsSchema.parse(invalidInput)).toThrow();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('SemanticSearchSchema', () => {
|
|
118
|
+
it('should validate correct search input', async () => {
|
|
119
|
+
const { z } = await import('zod');
|
|
120
|
+
|
|
121
|
+
const SemanticSearchSchema = z.object({
|
|
122
|
+
collection: z.string(),
|
|
123
|
+
query: z.string(),
|
|
124
|
+
limit: z.number().optional(),
|
|
125
|
+
filter: z.record(z.any()).optional(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const validInput = {
|
|
129
|
+
collection: 'test-collection',
|
|
130
|
+
query: 'search query',
|
|
131
|
+
limit: 10,
|
|
132
|
+
filter: { category: 'test' },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
expect(() => SemanticSearchSchema.parse(validInput)).not.toThrow();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should work with minimal input', async () => {
|
|
139
|
+
const { z } = await import('zod');
|
|
140
|
+
|
|
141
|
+
const SemanticSearchSchema = z.object({
|
|
142
|
+
collection: z.string(),
|
|
143
|
+
query: z.string(),
|
|
144
|
+
limit: z.number().optional(),
|
|
145
|
+
filter: z.record(z.any()).optional(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const minimalInput = {
|
|
149
|
+
collection: 'test',
|
|
150
|
+
query: 'search',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(() => SemanticSearchSchema.parse(minimalInput)).not.toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should require collection and query', async () => {
|
|
157
|
+
const { z } = await import('zod');
|
|
158
|
+
|
|
159
|
+
const SemanticSearchSchema = z.object({
|
|
160
|
+
collection: z.string(),
|
|
161
|
+
query: z.string(),
|
|
162
|
+
limit: z.number().optional(),
|
|
163
|
+
filter: z.record(z.any()).optional(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const missingQuery = { collection: 'test', limit: 5 };
|
|
167
|
+
expect(() => SemanticSearchSchema.parse(missingQuery)).toThrow();
|
|
168
|
+
|
|
169
|
+
const missingCollection = { query: 'test', limit: 5 };
|
|
170
|
+
expect(() => SemanticSearchSchema.parse(missingCollection)).toThrow();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('DeleteCollectionSchema', () => {
|
|
175
|
+
it('should validate correct delete input', async () => {
|
|
176
|
+
const { z } = await import('zod');
|
|
177
|
+
|
|
178
|
+
const DeleteCollectionSchema = z.object({
|
|
179
|
+
name: z.string(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const validInput = { name: 'test-collection' };
|
|
183
|
+
expect(() => DeleteCollectionSchema.parse(validInput)).not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should require name field', async () => {
|
|
187
|
+
const { z } = await import('zod');
|
|
188
|
+
|
|
189
|
+
const DeleteCollectionSchema = z.object({
|
|
190
|
+
name: z.string(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(() => DeleteCollectionSchema.parse({})).toThrow();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('GetCollectionInfoSchema', () => {
|
|
198
|
+
it('should validate correct input', async () => {
|
|
199
|
+
const { z } = await import('zod');
|
|
200
|
+
|
|
201
|
+
const GetCollectionInfoSchema = z.object({
|
|
202
|
+
name: z.string(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const validInput = { name: 'test-collection' };
|
|
206
|
+
expect(() => GetCollectionInfoSchema.parse(validInput)).not.toThrow();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('DeleteDocumentsSchema', () => {
|
|
211
|
+
it('should validate correct delete documents input', async () => {
|
|
212
|
+
const { z } = await import('zod');
|
|
213
|
+
|
|
214
|
+
const DeleteDocumentsSchema = z.object({
|
|
215
|
+
collection: z.string(),
|
|
216
|
+
ids: z.array(z.union([z.string(), z.number()])),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const validInput = {
|
|
220
|
+
collection: 'test-collection',
|
|
221
|
+
ids: [1, 'doc-2', 3],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
expect(() => DeleteDocumentsSchema.parse(validInput)).not.toThrow();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should accept string and number IDs', async () => {
|
|
228
|
+
const { z } = await import('zod');
|
|
229
|
+
|
|
230
|
+
const DeleteDocumentsSchema = z.object({
|
|
231
|
+
collection: z.string(),
|
|
232
|
+
ids: z.array(z.union([z.string(), z.number()])),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const stringIds = { collection: 'test', ids: ['a', 'b', 'c'] };
|
|
236
|
+
expect(() => DeleteDocumentsSchema.parse(stringIds)).not.toThrow();
|
|
237
|
+
|
|
238
|
+
const numberIds = { collection: 'test', ids: [1, 2, 3] };
|
|
239
|
+
expect(() => DeleteDocumentsSchema.parse(numberIds)).not.toThrow();
|
|
240
|
+
|
|
241
|
+
const mixedIds = { collection: 'test', ids: [1, 'b', 3] };
|
|
242
|
+
expect(() => DeleteDocumentsSchema.parse(mixedIds)).not.toThrow();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should require both collection and ids', async () => {
|
|
246
|
+
const { z } = await import('zod');
|
|
247
|
+
|
|
248
|
+
const DeleteDocumentsSchema = z.object({
|
|
249
|
+
collection: z.string(),
|
|
250
|
+
ids: z.array(z.union([z.string(), z.number()])),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const missingIds = { collection: 'test' };
|
|
254
|
+
expect(() => DeleteDocumentsSchema.parse(missingIds)).toThrow();
|
|
255
|
+
|
|
256
|
+
const missingCollection = { ids: [1, 2, 3] };
|
|
257
|
+
expect(() => DeleteDocumentsSchema.parse(missingCollection)).toThrow();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('MCP Server Resource URIs', () => {
|
|
263
|
+
it('should match collections URI pattern', () => {
|
|
264
|
+
const collectionsUri = 'qdrant://collections';
|
|
265
|
+
expect(collectionsUri).toMatch(/^qdrant:\/\/collections$/);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should match collection detail URI pattern', () => {
|
|
269
|
+
const collectionUri = 'qdrant://collection/my-collection';
|
|
270
|
+
const match = collectionUri.match(/^qdrant:\/\/collection\/(.+)$/);
|
|
271
|
+
|
|
272
|
+
expect(match).not.toBeNull();
|
|
273
|
+
expect(match![1]).toBe('my-collection');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should extract collection name from URI', () => {
|
|
277
|
+
const testCases = [
|
|
278
|
+
{ uri: 'qdrant://collection/test', expected: 'test' },
|
|
279
|
+
{ uri: 'qdrant://collection/my-docs', expected: 'my-docs' },
|
|
280
|
+
{ uri: 'qdrant://collection/collection-123', expected: 'collection-123' },
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
testCases.forEach(({ uri, expected }) => {
|
|
284
|
+
const match = uri.match(/^qdrant:\/\/collection\/(.+)$/);
|
|
285
|
+
expect(match![1]).toBe(expected);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should not match invalid URIs', () => {
|
|
290
|
+
const invalidUris = [
|
|
291
|
+
'qdrant://invalid',
|
|
292
|
+
'http://collections',
|
|
293
|
+
'qdrant://collection/',
|
|
294
|
+
'qdrant:collections',
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
invalidUris.forEach((uri) => {
|
|
298
|
+
const collectionsMatch = uri.match(/^qdrant:\/\/collections$/);
|
|
299
|
+
const collectionMatch = uri.match(/^qdrant:\/\/collection\/(.+)$/);
|
|
300
|
+
|
|
301
|
+
expect(collectionsMatch || collectionMatch).toBeFalsy();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|