@qwickapps/qwickbrain-proxy 1.0.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/publish.yml +92 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +45 -0
- package/README.md +165 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +142 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +23 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/schema.d.ts +551 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.js +202 -0
- package/dist/lib/__tests__/cache-manager.test.js.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.js +188 -0
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts +2 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.js +205 -0
- package/dist/lib/__tests__/proxy-server.test.js.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts +2 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js +233 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +25 -0
- package/dist/lib/cache-manager.d.ts.map +1 -0
- package/dist/lib/cache-manager.js +149 -0
- package/dist/lib/cache-manager.js.map +1 -0
- package/dist/lib/connection-manager.d.ts +26 -0
- package/dist/lib/connection-manager.d.ts.map +1 -0
- package/dist/lib/connection-manager.js +130 -0
- package/dist/lib/connection-manager.js.map +1 -0
- package/dist/lib/proxy-server.d.ts +19 -0
- package/dist/lib/proxy-server.d.ts.map +1 -0
- package/dist/lib/proxy-server.js +258 -0
- package/dist/lib/proxy-server.js.map +1 -0
- package/dist/lib/qwickbrain-client.d.ts +24 -0
- package/dist/lib/qwickbrain-client.d.ts.map +1 -0
- package/dist/lib/qwickbrain-client.js +197 -0
- package/dist/lib/qwickbrain-client.js.map +1 -0
- package/dist/types/config.d.ts +186 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +42 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/mcp.d.ts +223 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +78 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/drizzle/0000_fat_rafael_vega.sql +41 -0
- package/drizzle/0001_goofy_invisible_woman.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +276 -0
- package/drizzle/meta/0001_snapshot.json +295 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +12 -0
- package/package.json +65 -0
- package/src/bin/cli.ts +158 -0
- package/src/db/client.ts +34 -0
- package/src/db/schema.ts +68 -0
- package/src/index.ts +6 -0
- package/src/lib/__tests__/cache-manager.test.ts +264 -0
- package/src/lib/__tests__/connection-manager.test.ts +255 -0
- package/src/lib/__tests__/proxy-server.test.ts +261 -0
- package/src/lib/__tests__/qwickbrain-client.test.ts +310 -0
- package/src/lib/cache-manager.ts +201 -0
- package/src/lib/connection-manager.ts +156 -0
- package/src/lib/proxy-server.ts +320 -0
- package/src/lib/qwickbrain-client.ts +260 -0
- package/src/types/config.ts +47 -0
- package/src/types/mcp.ts +97 -0
- package/src/version.ts +11 -0
- package/test/fixtures/test-mcp.json +5 -0
- package/test-mcp-client.js +67 -0
- package/test-proxy.sh +25 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { ConnectionManager } from './connection-manager.js';
|
|
8
|
+
import { CacheManager } from './cache-manager.js';
|
|
9
|
+
import { QwickBrainClient } from './qwickbrain-client.js';
|
|
10
|
+
import type { Config } from '../types/config.js';
|
|
11
|
+
import type { DB } from '../db/client.js';
|
|
12
|
+
import type { MCPResponse, MCPResponseMetadata } from '../types/mcp.js';
|
|
13
|
+
import { VERSION } from '../version.js';
|
|
14
|
+
|
|
15
|
+
export class ProxyServer {
|
|
16
|
+
private server: Server;
|
|
17
|
+
private connectionManager: ConnectionManager;
|
|
18
|
+
private cacheManager: CacheManager;
|
|
19
|
+
private qwickbrainClient: QwickBrainClient;
|
|
20
|
+
private config: Config;
|
|
21
|
+
|
|
22
|
+
constructor(db: DB, config: Config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.server = new Server(
|
|
25
|
+
{
|
|
26
|
+
name: 'qwickbrain-proxy',
|
|
27
|
+
version: VERSION,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
capabilities: {
|
|
31
|
+
tools: {},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
this.qwickbrainClient = new QwickBrainClient(config.qwickbrain);
|
|
37
|
+
|
|
38
|
+
this.connectionManager = new ConnectionManager(
|
|
39
|
+
this.qwickbrainClient,
|
|
40
|
+
config.connection
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
this.cacheManager = new CacheManager(db, config.cache);
|
|
44
|
+
|
|
45
|
+
this.setupHandlers();
|
|
46
|
+
this.setupConnectionListeners();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private setupConnectionListeners(): void {
|
|
50
|
+
this.connectionManager.on('stateChange', ({ from, to }) => {
|
|
51
|
+
console.error(`Connection state: ${from} → ${to}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.connectionManager.on('reconnecting', ({ attempt, delay }) => {
|
|
55
|
+
console.error(`Reconnecting (attempt ${attempt}, delay ${delay}ms)...`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.connectionManager.on('connected', ({ latencyMs }) => {
|
|
59
|
+
console.error(`Connected to QwickBrain (latency: ${latencyMs}ms)`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.connectionManager.on('disconnected', ({ error }) => {
|
|
63
|
+
console.error(`Disconnected from QwickBrain: ${error}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private setupHandlers(): void {
|
|
68
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
69
|
+
tools: [
|
|
70
|
+
{
|
|
71
|
+
name: 'get_workflow',
|
|
72
|
+
description: 'Get a workflow definition by name',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
name: { type: 'string', description: 'Workflow name' },
|
|
77
|
+
},
|
|
78
|
+
required: ['name'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'get_document',
|
|
83
|
+
description: 'Get a document by name and type',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
name: { type: 'string', description: 'Document name' },
|
|
88
|
+
doc_type: { type: 'string', description: 'Document type (rule, frd, design, etc.)' },
|
|
89
|
+
project: { type: 'string', description: 'Project name (optional)' },
|
|
90
|
+
},
|
|
91
|
+
required: ['name', 'doc_type'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'get_memory',
|
|
96
|
+
description: 'Get a memory/context document by name',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
name: { type: 'string', description: 'Memory name' },
|
|
101
|
+
project: { type: 'string', description: 'Project name (optional)' },
|
|
102
|
+
},
|
|
103
|
+
required: ['name'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
110
|
+
const { name, arguments: args } = request.params;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
let result: MCPResponse;
|
|
114
|
+
|
|
115
|
+
switch (name) {
|
|
116
|
+
case 'get_workflow':
|
|
117
|
+
result = await this.handleGetWorkflow(args?.name as string);
|
|
118
|
+
break;
|
|
119
|
+
case 'get_document':
|
|
120
|
+
result = await this.handleGetDocument(
|
|
121
|
+
args?.doc_type as string,
|
|
122
|
+
args?.name as string,
|
|
123
|
+
args?.project as string | undefined
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
case 'get_memory':
|
|
127
|
+
result = await this.handleGetMemory(
|
|
128
|
+
args?.name as string,
|
|
129
|
+
args?.project as string | undefined
|
|
130
|
+
);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: JSON.stringify(result, null, 2),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: 'text',
|
|
150
|
+
text: JSON.stringify({
|
|
151
|
+
error: {
|
|
152
|
+
code: 'TOOL_ERROR',
|
|
153
|
+
message: errorMessage,
|
|
154
|
+
},
|
|
155
|
+
_metadata: {
|
|
156
|
+
source: 'cache',
|
|
157
|
+
status: this.connectionManager.getState(),
|
|
158
|
+
},
|
|
159
|
+
}, null, 2),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private createMetadata(source: 'live' | 'cache' | 'stale_cache', age?: number): MCPResponseMetadata {
|
|
169
|
+
return {
|
|
170
|
+
source,
|
|
171
|
+
age_seconds: age,
|
|
172
|
+
status: this.connectionManager.getState(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async handleGetWorkflow(name: string): Promise<MCPResponse> {
|
|
177
|
+
return this.handleGetDocument('workflow', name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async handleGetDocument(
|
|
181
|
+
docType: string,
|
|
182
|
+
name: string,
|
|
183
|
+
project?: string
|
|
184
|
+
): Promise<MCPResponse> {
|
|
185
|
+
// Try cache first
|
|
186
|
+
const cached = await this.cacheManager.getDocument(docType, name, project);
|
|
187
|
+
|
|
188
|
+
if (cached && !cached.isExpired) {
|
|
189
|
+
return {
|
|
190
|
+
data: cached.data,
|
|
191
|
+
_metadata: this.createMetadata('cache', cached.age),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Try remote if connected
|
|
196
|
+
if (this.connectionManager.getState() === 'connected') {
|
|
197
|
+
try {
|
|
198
|
+
const result = await this.connectionManager.execute(async () => {
|
|
199
|
+
return await this.qwickbrainClient.getDocument(docType, name, project);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Cache the result
|
|
203
|
+
await this.cacheManager.setDocument(
|
|
204
|
+
docType,
|
|
205
|
+
name,
|
|
206
|
+
result.content,
|
|
207
|
+
project,
|
|
208
|
+
result.metadata
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
data: result,
|
|
213
|
+
_metadata: this.createMetadata('live'),
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Failed to fetch from QwickBrain:', error);
|
|
217
|
+
// Fall through to stale cache
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Try stale cache
|
|
222
|
+
if (cached) {
|
|
223
|
+
return {
|
|
224
|
+
data: cached.data,
|
|
225
|
+
_metadata: {
|
|
226
|
+
...this.createMetadata('stale_cache', cached.age),
|
|
227
|
+
warning: `QwickBrain unavailable - serving cached data (${cached.age}s old)`,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// No cache, no connection
|
|
233
|
+
return {
|
|
234
|
+
error: {
|
|
235
|
+
code: 'UNAVAILABLE',
|
|
236
|
+
message: `QwickBrain unavailable and no cached data for ${docType}:${name}`,
|
|
237
|
+
suggestions: [
|
|
238
|
+
'Check internet connection',
|
|
239
|
+
'Wait for automatic reconnection',
|
|
240
|
+
docType === 'workflow' ? 'Try /plan command as fallback' : undefined,
|
|
241
|
+
].filter(Boolean) as string[],
|
|
242
|
+
},
|
|
243
|
+
_metadata: this.createMetadata('cache'),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async handleGetMemory(name: string, project?: string): Promise<MCPResponse> {
|
|
248
|
+
// Similar logic to handleGetDocument but for memories
|
|
249
|
+
const cached = await this.cacheManager.getMemory(name, project);
|
|
250
|
+
|
|
251
|
+
if (cached && !cached.isExpired) {
|
|
252
|
+
return {
|
|
253
|
+
data: cached.data,
|
|
254
|
+
_metadata: this.createMetadata('cache', cached.age),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this.connectionManager.getState() === 'connected') {
|
|
259
|
+
try {
|
|
260
|
+
const result = await this.connectionManager.execute(async () => {
|
|
261
|
+
return await this.qwickbrainClient.getMemory(name, project);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await this.cacheManager.setMemory(name, result.content, project, result.metadata);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
data: result,
|
|
268
|
+
_metadata: this.createMetadata('live'),
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Failed to fetch memory from QwickBrain:', error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (cached) {
|
|
276
|
+
return {
|
|
277
|
+
data: cached.data,
|
|
278
|
+
_metadata: {
|
|
279
|
+
...this.createMetadata('stale_cache', cached.age),
|
|
280
|
+
warning: `QwickBrain unavailable - serving cached memory (${cached.age}s old)`,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
error: {
|
|
287
|
+
code: 'UNAVAILABLE',
|
|
288
|
+
message: `QwickBrain unavailable and no cached memory: ${name}`,
|
|
289
|
+
suggestions: ['Check connection', 'Wait for reconnection'],
|
|
290
|
+
},
|
|
291
|
+
_metadata: this.createMetadata('cache'),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async start(): Promise<void> {
|
|
296
|
+
// Clean up expired cache entries on startup
|
|
297
|
+
const { documentsDeleted, memoriesDeleted } = await this.cacheManager.cleanupExpiredEntries();
|
|
298
|
+
if (documentsDeleted > 0 || memoriesDeleted > 0) {
|
|
299
|
+
console.error(`Cache cleanup: removed ${documentsDeleted} documents, ${memoriesDeleted} memories`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Connect to QwickBrain
|
|
303
|
+
await this.qwickbrainClient.connect();
|
|
304
|
+
|
|
305
|
+
// Start connection manager
|
|
306
|
+
await this.connectionManager.start();
|
|
307
|
+
|
|
308
|
+
// Start MCP server
|
|
309
|
+
const transport = new StdioServerTransport();
|
|
310
|
+
await this.server.connect(transport);
|
|
311
|
+
|
|
312
|
+
console.error('QwickBrain Proxy started');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async stop(): Promise<void> {
|
|
316
|
+
this.connectionManager.stop();
|
|
317
|
+
await this.qwickbrainClient.disconnect();
|
|
318
|
+
await this.server.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
4
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
5
|
+
import type { Config } from '../types/config.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { VERSION } from '../version.js';
|
|
8
|
+
|
|
9
|
+
// Zod schema for MCP tool response validation
|
|
10
|
+
const MCPToolResponseSchema = z.object({
|
|
11
|
+
content: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
type: z.literal('text'),
|
|
14
|
+
text: z.string(),
|
|
15
|
+
})
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Zod schema for the document/memory data inside the MCP response
|
|
20
|
+
const QwickBrainDocumentSchema = z.object({
|
|
21
|
+
document: z
|
|
22
|
+
.object({
|
|
23
|
+
content: z.string(),
|
|
24
|
+
metadata: z.record(z.unknown()).optional(),
|
|
25
|
+
})
|
|
26
|
+
.optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Zod schema for HTTP API responses
|
|
30
|
+
const HTTPResponseSchema = z.object({
|
|
31
|
+
content: z.string(),
|
|
32
|
+
metadata: z.record(z.unknown()).optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export interface QwickBrainResponse {
|
|
36
|
+
content: string;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class QwickBrainClient {
|
|
41
|
+
private client: Client | null = null;
|
|
42
|
+
private transport: Transport | null = null;
|
|
43
|
+
private mode: 'mcp' | 'http' | 'sse';
|
|
44
|
+
private config: Config['qwickbrain'];
|
|
45
|
+
|
|
46
|
+
constructor(config: Config['qwickbrain']) {
|
|
47
|
+
this.mode = config.mode;
|
|
48
|
+
this.config = config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async connect(): Promise<void> {
|
|
52
|
+
if (this.mode === 'mcp') {
|
|
53
|
+
await this.connectMCP();
|
|
54
|
+
} else if (this.mode === 'sse') {
|
|
55
|
+
await this.connectSSE();
|
|
56
|
+
}
|
|
57
|
+
// HTTP mode doesn't need persistent connection
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async connectMCP(): Promise<void> {
|
|
61
|
+
if (!this.config.command) {
|
|
62
|
+
throw new Error('MCP mode requires command to be configured');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.client = new Client(
|
|
66
|
+
{
|
|
67
|
+
name: 'qwickbrain-proxy',
|
|
68
|
+
version: VERSION,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
capabilities: {},
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
this.transport = new StdioClientTransport({
|
|
76
|
+
command: this.config.command,
|
|
77
|
+
args: this.config.args || [],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await this.client.connect(this.transport);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async connectSSE(): Promise<void> {
|
|
84
|
+
if (!this.config.url) {
|
|
85
|
+
throw new Error('SSE mode requires url to be configured');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.client = new Client(
|
|
89
|
+
{
|
|
90
|
+
name: 'qwickbrain-proxy',
|
|
91
|
+
version: VERSION,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
capabilities: {},
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
this.transport = new SSEClientTransport(
|
|
99
|
+
new URL(this.config.url)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await this.client.connect(this.transport);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getDocument(
|
|
106
|
+
docType: string,
|
|
107
|
+
name: string,
|
|
108
|
+
project?: string
|
|
109
|
+
): Promise<QwickBrainResponse> {
|
|
110
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
111
|
+
return this.getDocumentMCP(docType, name, project);
|
|
112
|
+
} else {
|
|
113
|
+
return this.getDocumentHTTP(docType, name, project);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async getDocumentMCP(
|
|
118
|
+
docType: string,
|
|
119
|
+
name: string,
|
|
120
|
+
project?: string
|
|
121
|
+
): Promise<QwickBrainResponse> {
|
|
122
|
+
if (!this.client) {
|
|
123
|
+
throw new Error('MCP client not connected');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = await this.client.callTool({
|
|
127
|
+
name: 'get_document',
|
|
128
|
+
arguments: {
|
|
129
|
+
doc_type: docType,
|
|
130
|
+
name,
|
|
131
|
+
project,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Validate MCP response structure
|
|
136
|
+
const parsed = MCPToolResponseSchema.parse(result);
|
|
137
|
+
const data = QwickBrainDocumentSchema.parse(JSON.parse(parsed.content[0].text));
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: data.document?.content || '',
|
|
141
|
+
metadata: data.document?.metadata,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async getDocumentHTTP(
|
|
146
|
+
docType: string,
|
|
147
|
+
name: string,
|
|
148
|
+
project?: string
|
|
149
|
+
): Promise<QwickBrainResponse> {
|
|
150
|
+
if (!this.config.url) {
|
|
151
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await fetch(`${this.config.url}/mcp/document`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({ doc_type: docType, name, project }),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const json = await response.json();
|
|
168
|
+
return HTTPResponseSchema.parse(json);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getMemory(name: string, project?: string): Promise<QwickBrainResponse> {
|
|
172
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
173
|
+
return this.getMemoryMCP(name, project);
|
|
174
|
+
} else {
|
|
175
|
+
return this.getMemoryHTTP(name, project);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async getMemoryMCP(
|
|
180
|
+
name: string,
|
|
181
|
+
project?: string
|
|
182
|
+
): Promise<QwickBrainResponse> {
|
|
183
|
+
if (!this.client) {
|
|
184
|
+
throw new Error('MCP client not connected');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = await this.client.callTool({
|
|
188
|
+
name: 'get_memory',
|
|
189
|
+
arguments: {
|
|
190
|
+
name,
|
|
191
|
+
project,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Validate MCP response structure
|
|
196
|
+
const parsed = MCPToolResponseSchema.parse(result);
|
|
197
|
+
const data = QwickBrainDocumentSchema.parse(JSON.parse(parsed.content[0].text));
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
content: data.document?.content || '',
|
|
201
|
+
metadata: data.document?.metadata,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async getMemoryHTTP(
|
|
206
|
+
name: string,
|
|
207
|
+
project?: string
|
|
208
|
+
): Promise<QwickBrainResponse> {
|
|
209
|
+
if (!this.config.url) {
|
|
210
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const response = await fetch(`${this.config.url}/mcp/memory`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({ name, project }),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const json = await response.json();
|
|
227
|
+
return HTTPResponseSchema.parse(json);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async healthCheck(): Promise<boolean> {
|
|
231
|
+
try {
|
|
232
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
233
|
+
// For MCP/SSE mode, check if client is connected
|
|
234
|
+
if (!this.client) {
|
|
235
|
+
await this.connect();
|
|
236
|
+
}
|
|
237
|
+
// Try listing tools as health check
|
|
238
|
+
await this.client!.listTools();
|
|
239
|
+
return true;
|
|
240
|
+
} else {
|
|
241
|
+
// For HTTP mode, ping health endpoint
|
|
242
|
+
if (!this.config.url) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const response = await fetch(`${this.config.url}/health`);
|
|
246
|
+
return response.ok;
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async disconnect(): Promise<void> {
|
|
254
|
+
if ((this.mode === 'mcp' || this.mode === 'sse') && this.client && this.transport) {
|
|
255
|
+
await this.client.close();
|
|
256
|
+
this.client = null;
|
|
257
|
+
this.transport = null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const ConfigSchema = z.object({
|
|
4
|
+
qwickbrain: z.object({
|
|
5
|
+
mode: z.enum(['mcp', 'http', 'sse']).default('sse'),
|
|
6
|
+
// For MCP mode (local stdio)
|
|
7
|
+
command: z.string().optional(),
|
|
8
|
+
args: z.array(z.string()).optional(),
|
|
9
|
+
// For HTTP mode (cloud/remote) or SSE mode
|
|
10
|
+
url: z.string().url().optional(),
|
|
11
|
+
apiKey: z.string().optional(),
|
|
12
|
+
}).default({}),
|
|
13
|
+
cache: z.object({
|
|
14
|
+
dir: z.string().optional(),
|
|
15
|
+
ttl: z.object({
|
|
16
|
+
workflows: z.number().default(86400), // 24 hours
|
|
17
|
+
rules: z.number().default(86400), // 24 hours
|
|
18
|
+
documents: z.number().default(21600), // 6 hours
|
|
19
|
+
memories: z.number().default(3600), // 1 hour
|
|
20
|
+
}).default({}),
|
|
21
|
+
preload: z.array(z.string()).default(['workflows', 'rules']),
|
|
22
|
+
}).default({}),
|
|
23
|
+
connection: z.object({
|
|
24
|
+
healthCheckInterval: z.number().default(30000), // 30 seconds
|
|
25
|
+
timeout: z.number().default(5000), // 5 seconds
|
|
26
|
+
maxReconnectAttempts: z.number().default(10),
|
|
27
|
+
reconnectBackoff: z.object({
|
|
28
|
+
initial: z.number().default(1000), // 1 second
|
|
29
|
+
max: z.number().default(60000), // 60 seconds
|
|
30
|
+
multiplier: z.number().default(2),
|
|
31
|
+
}).default({}),
|
|
32
|
+
}).default({}),
|
|
33
|
+
sync: z.object({
|
|
34
|
+
interval: z.number().default(300000), // 5 minutes
|
|
35
|
+
batchSize: z.number().default(10),
|
|
36
|
+
}).default({}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
40
|
+
|
|
41
|
+
export const CacheConfigSchema = z.object({
|
|
42
|
+
ttl: z.number(),
|
|
43
|
+
offline: z.boolean(),
|
|
44
|
+
preload: z.boolean(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type CacheConfig = z.infer<typeof CacheConfigSchema>;
|