mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
fix: MCP file ops use the connected OperationContext engine, not the global DB singleton (#3869)
Wave-assembled from PR #3869 by @dpaluy. Co-Authored-By: David Paluy <dpaluy@users.noreply.github.com>
This commit is contained in:
committed by
Sina Matian
co-authored by
David Paluy
parent
0a34ced5d7
commit
f7d63c7159
@@ -3246,8 +3246,9 @@ const file_list: Operation = {
|
||||
},
|
||||
scope: 'admin',
|
||||
localOnly: true,
|
||||
handler: async (_ctx, p) => {
|
||||
const sql = db.getConnection();
|
||||
handler: async (ctx, p) => {
|
||||
const { sqlQueryForEngine } = await import('./sql-query.ts');
|
||||
const sql = sqlQueryForEngine(ctx.engine);
|
||||
const slug = p.slug as string | undefined;
|
||||
const rows = slug
|
||||
? await sql`SELECT id, page_slug, filename, storage_path, mime_type, size_bytes, content_hash, created_at FROM files WHERE page_slug = ${slug} ORDER BY filename LIMIT ${FILE_LIST_LIMIT}`
|
||||
@@ -3304,7 +3305,8 @@ const file_upload: Operation = {
|
||||
};
|
||||
const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] || null;
|
||||
|
||||
const sql = db.getConnection();
|
||||
const { sqlQueryForEngine } = await import('./sql-query.ts');
|
||||
const sql = sqlQueryForEngine(ctx.engine);
|
||||
const existing = await sql`SELECT id FROM files WHERE content_hash = ${hash} AND storage_path = ${storagePath}`;
|
||||
if (existing.length > 0) {
|
||||
return { status: 'already_exists', storage_path: storagePath };
|
||||
@@ -3354,8 +3356,9 @@ const file_url: Operation = {
|
||||
},
|
||||
scope: 'admin',
|
||||
localOnly: true,
|
||||
handler: async (_ctx, p) => {
|
||||
const sql = db.getConnection();
|
||||
handler: async (ctx, p) => {
|
||||
const { sqlQueryForEngine } = await import('./sql-query.ts');
|
||||
const sql = sqlQueryForEngine(ctx.engine);
|
||||
const rows = await sql`SELECT storage_path, mime_type, size_bytes FROM files WHERE storage_path = ${p.storage_path as string}`;
|
||||
if (rows.length === 0) {
|
||||
throw new OperationError('storage_error', `File not found: ${p.storage_path}`);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Regression: MCP file_upload must use the connected OperationContext engine.
|
||||
*
|
||||
* A long-running `gbrain serve` owns the PGLite connection. The handler must not
|
||||
* reach for the module-global db singleton, which is intentionally uninitialized
|
||||
* in the MCP dispatch path and throws "connect() has not been called".
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { dispatchToolCall } from '../src/mcp/dispatch.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let fixtureDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({ engine: 'pglite' } as never);
|
||||
await engine.initSchema();
|
||||
|
||||
// Remote MCP uploads are intentionally confined to the server working tree.
|
||||
fixtureDir = mkdtempSync(join(process.cwd(), '.file-upload-engine-context-'));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (engine) await engine.disconnect();
|
||||
if (fixtureDir) rmSync(fixtureDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('file_upload engine ownership', () => {
|
||||
test('uses the MCP context engine instead of the module-global DB singleton', async () => {
|
||||
const fixture = join(fixtureDir, 'capture.json');
|
||||
writeFileSync(fixture, '{"source":"camofox"}\n');
|
||||
|
||||
const result = await dispatchToolCall(engine, 'file_upload', {
|
||||
path: fixture,
|
||||
page_slug: 'concepts/hermes-kanban',
|
||||
}, { remote: true, sourceId: 'default' });
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(JSON.parse(result.content[0].text)).toEqual({
|
||||
status: 'uploaded',
|
||||
storage_path: 'concepts/hermes-kanban/capture.json',
|
||||
size_bytes: 21,
|
||||
});
|
||||
|
||||
const listed = await dispatchToolCall(engine, 'file_list', {
|
||||
slug: 'concepts/hermes-kanban',
|
||||
}, { remote: true, sourceId: 'default' });
|
||||
expect(listed.isError).toBeFalsy();
|
||||
expect(JSON.parse(listed.content[0].text)).toEqual([
|
||||
expect.objectContaining({
|
||||
page_slug: 'concepts/hermes-kanban',
|
||||
storage_path: 'concepts/hermes-kanban/capture.json',
|
||||
}),
|
||||
]);
|
||||
|
||||
const url = await dispatchToolCall(engine, 'file_url', {
|
||||
storage_path: 'concepts/hermes-kanban/capture.json',
|
||||
}, { remote: true, sourceId: 'default' });
|
||||
expect(url.isError).toBeFalsy();
|
||||
expect(JSON.parse(url.content[0].text)).toEqual({
|
||||
storage_path: 'concepts/hermes-kanban/capture.json',
|
||||
url: 'gbrain:files/concepts/hermes-kanban/capture.json',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user