@nubase_ai/mcp-bridge 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 +83 -0
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.js +23 -0
- package/dist/src/context.d.ts +13 -0
- package/dist/src/context.js +11 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +43 -0
- package/dist/src/mcp-stdio.d.ts +19 -0
- package/dist/src/mcp-stdio.js +71 -0
- package/dist/src/nubase-client.d.ts +19 -0
- package/dist/src/nubase-client.js +117 -0
- package/dist/src/sql-risk.d.ts +3 -0
- package/dist/src/sql-risk.js +51 -0
- package/dist/src/tools.d.ts +9 -0
- package/dist/src/tools.js +94 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @nubase_ai/mcp-bridge
|
|
2
|
+
|
|
3
|
+
stdio MCP bridge for Nubase. Use it when Codex, Claude Code, Cursor, IDEA, or another local MCP client needs Nubase tools.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @nubase_ai/mcp-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For local development inside the Nubase repository:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd frontend
|
|
15
|
+
pnpm --filter @nubase_ai/mcp-bridge build
|
|
16
|
+
node packages/mcp-bridge/dist/src/index.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## MCP Config
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"nubase": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["@nubase_ai/mcp-bridge"],
|
|
27
|
+
"env": {
|
|
28
|
+
"NUBASE_URL": "http://localhost:9999",
|
|
29
|
+
"NUBASE_PROJECT_KEY": "YOUR_NUBASE_PROJECT_KEY",
|
|
30
|
+
"NUBASE_AGENT_ID": "claude-code"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Context Injection
|
|
38
|
+
|
|
39
|
+
The bridge injects user and session context from environment variables:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
NUBASE_URL=http://localhost:9999
|
|
43
|
+
NUBASE_PROJECT_KEY=YOUR_NUBASE_PROJECT_KEY
|
|
44
|
+
NUBASE_USER_JWT=USER_ACCESS_TOKEN
|
|
45
|
+
NUBASE_USER_ID=USER_UUID
|
|
46
|
+
NUBASE_AGENT_ID=codex
|
|
47
|
+
NUBASE_RUN_ID=feature-123
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tool arguments can override `userId`, `agentId`, and `runId` for one call. API keys and JWTs stay in the bridge process environment and are not exposed as tool arguments.
|
|
51
|
+
|
|
52
|
+
## SQL Safety
|
|
53
|
+
|
|
54
|
+
SQL execution is disabled by default:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
NUBASE_ALLOW_SQL_EXECUTE=true
|
|
58
|
+
NUBASE_ALLOW_DANGEROUS_SQL=false
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `sql_dry_run` before `sql_execute`. Dangerous SQL remains blocked unless `NUBASE_ALLOW_DANGEROUS_SQL=true`.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
- `nubase_capabilities`
|
|
66
|
+
- `nubase_instructions`
|
|
67
|
+
- `memory_context`
|
|
68
|
+
- `memory_search`
|
|
69
|
+
- `memory_write`
|
|
70
|
+
- `rest_select`
|
|
71
|
+
- `sql_dry_run`
|
|
72
|
+
- `sql_execute`
|
|
73
|
+
|
|
74
|
+
## Publish
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm --filter @nubase_ai/mcp-bridge typecheck
|
|
78
|
+
pnpm --filter @nubase_ai/mcp-bridge build
|
|
79
|
+
pnpm --filter @nubase_ai/mcp-bridge test
|
|
80
|
+
pnpm --filter @nubase_ai/mcp-bridge pack:check
|
|
81
|
+
cd packages/mcp-bridge
|
|
82
|
+
npm publish --access public
|
|
83
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BridgeConfig {
|
|
2
|
+
nubaseUrl: string;
|
|
3
|
+
projectKey: string;
|
|
4
|
+
userJwt?: string;
|
|
5
|
+
agentId?: string;
|
|
6
|
+
userId?: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
allowSqlExecute: boolean;
|
|
9
|
+
allowDangerousSql: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): BridgeConfig;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function loadConfig(env = process.env) {
|
|
2
|
+
const nubaseUrl = stripTrailingSlash(env.NUBASE_URL || 'http://localhost:9999');
|
|
3
|
+
const projectKey = env.NUBASE_PROJECT_KEY || env.NUBASE_API_KEY || '';
|
|
4
|
+
return {
|
|
5
|
+
nubaseUrl,
|
|
6
|
+
projectKey,
|
|
7
|
+
userJwt: blankToUndefined(env.NUBASE_USER_JWT),
|
|
8
|
+
agentId: blankToUndefined(env.NUBASE_AGENT_ID),
|
|
9
|
+
userId: blankToUndefined(env.NUBASE_USER_ID),
|
|
10
|
+
runId: blankToUndefined(env.NUBASE_RUN_ID),
|
|
11
|
+
allowSqlExecute: truthy(env.NUBASE_ALLOW_SQL_EXECUTE),
|
|
12
|
+
allowDangerousSql: truthy(env.NUBASE_ALLOW_DANGEROUS_SQL),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function stripTrailingSlash(value) {
|
|
16
|
+
return value.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
function blankToUndefined(value) {
|
|
19
|
+
return value && value.trim() ? value.trim() : undefined;
|
|
20
|
+
}
|
|
21
|
+
function truthy(value) {
|
|
22
|
+
return ['1', 'true', 'yes', 'on'].includes((value || '').toLowerCase());
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BridgeConfig } from './config.js';
|
|
2
|
+
export interface ScopeArgs {
|
|
3
|
+
userId?: string;
|
|
4
|
+
agentId?: string;
|
|
5
|
+
runId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedScope {
|
|
8
|
+
userId?: string;
|
|
9
|
+
agentId?: string;
|
|
10
|
+
runId?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveScope(config: BridgeConfig, args?: ScopeArgs): ResolvedScope;
|
|
13
|
+
export declare function withScope<T extends Record<string, unknown>>(config: BridgeConfig, args: T & ScopeArgs): T & ResolvedScope;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function resolveScope(config, args = {}) {
|
|
2
|
+
return {
|
|
3
|
+
userId: args.userId || config.userId,
|
|
4
|
+
agentId: args.agentId || config.agentId,
|
|
5
|
+
runId: args.runId || config.runId,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function withScope(config, args) {
|
|
9
|
+
const scope = resolveScope(config, args);
|
|
10
|
+
return Object.fromEntries(Object.entries({ ...args, ...scope }).filter(([, value]) => value !== undefined && value !== ''));
|
|
11
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { McpStdioServer } from './mcp-stdio.js';
|
|
4
|
+
import { NubaseClient } from './nubase-client.js';
|
|
5
|
+
import { callTool, TOOLS } from './tools.js';
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const client = new NubaseClient(config);
|
|
8
|
+
const server = new McpStdioServer(async (request) => {
|
|
9
|
+
switch (request.method) {
|
|
10
|
+
case 'initialize':
|
|
11
|
+
return {
|
|
12
|
+
protocolVersion: request.params?.protocolVersion ?? '2024-11-05',
|
|
13
|
+
capabilities: { tools: {} },
|
|
14
|
+
serverInfo: { name: '@nubase_ai/mcp-bridge', version: '0.1.0' },
|
|
15
|
+
};
|
|
16
|
+
case 'notifications/initialized':
|
|
17
|
+
return null;
|
|
18
|
+
case 'tools/list':
|
|
19
|
+
return {
|
|
20
|
+
tools: TOOLS.map((tool) => ({
|
|
21
|
+
name: tool.name,
|
|
22
|
+
description: tool.description,
|
|
23
|
+
inputSchema: tool.inputSchema,
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
case 'tools/call': {
|
|
27
|
+
const name = request.params?.name;
|
|
28
|
+
const args = request.params?.arguments ?? {};
|
|
29
|
+
const result = await callTool(name, args, config, client);
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: JSON.stringify(result, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unsupported method: ${request.method}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
server.start();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface JsonRpcRequest {
|
|
2
|
+
jsonrpc: '2.0';
|
|
3
|
+
id?: string | number;
|
|
4
|
+
method: string;
|
|
5
|
+
params?: any;
|
|
6
|
+
}
|
|
7
|
+
type Handler = (request: JsonRpcRequest) => Promise<unknown> | unknown;
|
|
8
|
+
export declare class McpStdioServer {
|
|
9
|
+
private readonly handler;
|
|
10
|
+
private readonly events;
|
|
11
|
+
private buffer;
|
|
12
|
+
constructor(handler: Handler);
|
|
13
|
+
start(): void;
|
|
14
|
+
private onData;
|
|
15
|
+
private handleMessage;
|
|
16
|
+
private readMessage;
|
|
17
|
+
private write;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
export class McpStdioServer {
|
|
4
|
+
handler;
|
|
5
|
+
events = new EventEmitter();
|
|
6
|
+
buffer = Buffer.alloc(0);
|
|
7
|
+
constructor(handler) {
|
|
8
|
+
this.handler = handler;
|
|
9
|
+
}
|
|
10
|
+
start() {
|
|
11
|
+
stdin.on('data', (chunk) => this.onData(chunk));
|
|
12
|
+
stdin.resume();
|
|
13
|
+
}
|
|
14
|
+
onData(chunk) {
|
|
15
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
16
|
+
while (true) {
|
|
17
|
+
const next = this.readMessage();
|
|
18
|
+
if (!next)
|
|
19
|
+
return;
|
|
20
|
+
this.handleMessage(next).catch((error) => {
|
|
21
|
+
this.write({
|
|
22
|
+
jsonrpc: '2.0',
|
|
23
|
+
id: next.id ?? null,
|
|
24
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async handleMessage(request) {
|
|
30
|
+
if (request.id === undefined) {
|
|
31
|
+
await this.handler(request);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = await this.handler(request);
|
|
36
|
+
this.write({ jsonrpc: '2.0', id: request.id, result });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.write({
|
|
40
|
+
jsonrpc: '2.0',
|
|
41
|
+
id: request.id,
|
|
42
|
+
error: {
|
|
43
|
+
code: -32603,
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
readMessage() {
|
|
50
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
51
|
+
if (headerEnd === -1)
|
|
52
|
+
return null;
|
|
53
|
+
const header = this.buffer.subarray(0, headerEnd).toString('utf8');
|
|
54
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new Error('Missing Content-Length header');
|
|
57
|
+
}
|
|
58
|
+
const length = Number(match[1]);
|
|
59
|
+
const bodyStart = headerEnd + 4;
|
|
60
|
+
const bodyEnd = bodyStart + length;
|
|
61
|
+
if (this.buffer.length < bodyEnd)
|
|
62
|
+
return null;
|
|
63
|
+
const body = this.buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
64
|
+
this.buffer = this.buffer.subarray(bodyEnd);
|
|
65
|
+
return JSON.parse(body);
|
|
66
|
+
}
|
|
67
|
+
write(message) {
|
|
68
|
+
const body = JSON.stringify(message);
|
|
69
|
+
stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BridgeConfig } from './config.js';
|
|
2
|
+
export declare class NubaseClient {
|
|
3
|
+
private readonly config;
|
|
4
|
+
constructor(config: BridgeConfig);
|
|
5
|
+
capabilities(): Promise<any>;
|
|
6
|
+
instructions(): Promise<any>;
|
|
7
|
+
memoryContext(args: Record<string, unknown>): Promise<any>;
|
|
8
|
+
memorySearch(args: Record<string, unknown>): Promise<any>;
|
|
9
|
+
memoryWrite(args: Record<string, unknown>): Promise<any>;
|
|
10
|
+
restSelect(args: Record<string, unknown>): Promise<any>;
|
|
11
|
+
sqlDryRun(args: Record<string, unknown>): {
|
|
12
|
+
success: boolean;
|
|
13
|
+
risk: import("./sql-risk.js").SqlRisk;
|
|
14
|
+
statementCount: number;
|
|
15
|
+
executable: boolean;
|
|
16
|
+
};
|
|
17
|
+
sqlExecute(args: Record<string, unknown>): Promise<any>;
|
|
18
|
+
private request;
|
|
19
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { classifySql, countStatements } from './sql-risk.js';
|
|
2
|
+
export class NubaseClient {
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
capabilities() {
|
|
8
|
+
return this.request('/agent/v1/capabilities');
|
|
9
|
+
}
|
|
10
|
+
instructions() {
|
|
11
|
+
return this.request('/agent/v1/instructions');
|
|
12
|
+
}
|
|
13
|
+
memoryContext(args) {
|
|
14
|
+
return this.request('/mem/v1/search', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
body: {
|
|
17
|
+
userId: args.userId,
|
|
18
|
+
agentId: args.agentId,
|
|
19
|
+
runId: args.runId,
|
|
20
|
+
query: args.task || args.query,
|
|
21
|
+
topK: args.topK ?? 8,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
memorySearch(args) {
|
|
26
|
+
return this.request('/mem/v1/search', { method: 'POST', body: args });
|
|
27
|
+
}
|
|
28
|
+
memoryWrite(args) {
|
|
29
|
+
return this.request('/mem/v1/memories', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: {
|
|
32
|
+
userId: args.userId,
|
|
33
|
+
agentId: args.agentId,
|
|
34
|
+
runId: args.runId,
|
|
35
|
+
infer: args.infer ?? true,
|
|
36
|
+
messages: [{ role: 'user', content: args.content }],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
restSelect(args) {
|
|
41
|
+
const table = requiredString(args.table, 'table');
|
|
42
|
+
const query = typeof args.query === 'string' && args.query ? args.query : 'select=*';
|
|
43
|
+
return this.request(`/rest/v1/${encodeURIComponent(table)}?${query}`);
|
|
44
|
+
}
|
|
45
|
+
sqlDryRun(args) {
|
|
46
|
+
const sql = requiredString(args.sql, 'sql');
|
|
47
|
+
const risk = classifySql(sql);
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
risk,
|
|
51
|
+
statementCount: countStatements(sql),
|
|
52
|
+
executable: risk !== 'DANGEROUS',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async sqlExecute(args) {
|
|
56
|
+
const sql = requiredString(args.sql, 'sql');
|
|
57
|
+
const dryRun = this.sqlDryRun({ sql });
|
|
58
|
+
if (!this.config.allowSqlExecute) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'SQL execution is disabled. Set NUBASE_ALLOW_SQL_EXECUTE=true to enable it.',
|
|
62
|
+
dryRun,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (dryRun.risk === 'DANGEROUS' && !this.config.allowDangerousSql) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'Dangerous SQL is blocked. Set NUBASE_ALLOW_DANGEROUS_SQL=true to allow it.',
|
|
69
|
+
dryRun,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const result = await this.request('/auth/v1/admin/sql/execute', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: { query: sql },
|
|
75
|
+
});
|
|
76
|
+
return { risk: dryRun.risk, ...result };
|
|
77
|
+
}
|
|
78
|
+
async request(path, options = {}) {
|
|
79
|
+
if (!this.config.projectKey) {
|
|
80
|
+
throw new Error('Missing NUBASE_PROJECT_KEY or NUBASE_API_KEY.');
|
|
81
|
+
}
|
|
82
|
+
const headers = {
|
|
83
|
+
apikey: this.config.projectKey,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
};
|
|
86
|
+
if (this.config.userJwt) {
|
|
87
|
+
headers.Authorization = `Bearer ${this.config.userJwt}`;
|
|
88
|
+
}
|
|
89
|
+
const response = await fetch(`${this.config.nubaseUrl}${path}`, {
|
|
90
|
+
method: options.method || 'GET',
|
|
91
|
+
headers,
|
|
92
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
93
|
+
});
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
const data = parseResponse(text);
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(typeof data === 'string' ? data : JSON.stringify(data));
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parseResponse(text) {
|
|
103
|
+
if (!text)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(text);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function requiredString(value, name) {
|
|
113
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
114
|
+
throw new Error(`${name} is required`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function classifySql(sql) {
|
|
2
|
+
const statements = splitStatements(sql);
|
|
3
|
+
if (statements.length === 0)
|
|
4
|
+
return 'UNKNOWN';
|
|
5
|
+
return statements.reduce((highest, statement) => maxRisk(highest, classifyStatement(statement)), 'UNKNOWN');
|
|
6
|
+
}
|
|
7
|
+
export function countStatements(sql) {
|
|
8
|
+
return splitStatements(sql).length;
|
|
9
|
+
}
|
|
10
|
+
function classifyStatement(statement) {
|
|
11
|
+
const normalized = statement
|
|
12
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
13
|
+
.replace(/--.*$/gm, ' ')
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
if (!normalized)
|
|
18
|
+
return 'UNKNOWN';
|
|
19
|
+
if (startsWithAny(normalized, ['drop ', 'truncate ', 'reindex ', 'vacuum full', 'cluster ']))
|
|
20
|
+
return 'DANGEROUS';
|
|
21
|
+
if (/^delete\s+from\s+[^\s;]+\s*$/.test(normalized))
|
|
22
|
+
return 'DANGEROUS';
|
|
23
|
+
if (startsWithAny(normalized, ['create ', 'alter ', 'grant ', 'revoke ', 'comment ', 'security label '])) {
|
|
24
|
+
return 'SCHEMA_WRITE';
|
|
25
|
+
}
|
|
26
|
+
if (startsWithAny(normalized, ['insert ', 'update ', 'delete ', 'merge ', 'copy ', 'call ']))
|
|
27
|
+
return 'DATA_WRITE';
|
|
28
|
+
if (startsWithAny(normalized, ['select ', 'with ', 'show ', 'explain ', 'describe ']))
|
|
29
|
+
return 'READ';
|
|
30
|
+
return 'UNKNOWN';
|
|
31
|
+
}
|
|
32
|
+
function splitStatements(sql) {
|
|
33
|
+
if (!sql || !sql.trim())
|
|
34
|
+
return [];
|
|
35
|
+
return sql.split(';').map((s) => s.trim()).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
function startsWithAny(value, prefixes) {
|
|
38
|
+
return prefixes.some((prefix) => value.startsWith(prefix));
|
|
39
|
+
}
|
|
40
|
+
function maxRisk(left, right) {
|
|
41
|
+
return severity(left) >= severity(right) ? left : right;
|
|
42
|
+
}
|
|
43
|
+
function severity(risk) {
|
|
44
|
+
return {
|
|
45
|
+
UNKNOWN: 0,
|
|
46
|
+
READ: 1,
|
|
47
|
+
DATA_WRITE: 2,
|
|
48
|
+
SCHEMA_WRITE: 3,
|
|
49
|
+
DANGEROUS: 4,
|
|
50
|
+
}[risk];
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BridgeConfig } from './config.js';
|
|
2
|
+
import type { NubaseClient } from './nubase-client.js';
|
|
3
|
+
export interface ToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export declare const TOOLS: ToolDefinition[];
|
|
9
|
+
export declare function callTool(name: string, args: Record<string, unknown>, config: BridgeConfig, client: NubaseClient): Promise<any>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { withScope } from './context.js';
|
|
2
|
+
export const TOOLS = [
|
|
3
|
+
{
|
|
4
|
+
name: 'nubase_capabilities',
|
|
5
|
+
description: 'Discover Nubase backend capabilities and stable API paths.',
|
|
6
|
+
inputSchema: objectSchema({}),
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'nubase_instructions',
|
|
10
|
+
description: 'Return agent instructions for using Nubase safely.',
|
|
11
|
+
inputSchema: objectSchema({}),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'memory_context',
|
|
15
|
+
description: 'Return compact relevant memory context for a task. Scope defaults can come from NUBASE_USER_ID, NUBASE_AGENT_ID, and NUBASE_RUN_ID.',
|
|
16
|
+
inputSchema: objectSchema({
|
|
17
|
+
task: { type: 'string' },
|
|
18
|
+
topK: { type: 'number' },
|
|
19
|
+
userId: { type: 'string' },
|
|
20
|
+
agentId: { type: 'string' },
|
|
21
|
+
runId: { type: 'string' },
|
|
22
|
+
}, ['task']),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'memory_search',
|
|
26
|
+
description: 'Search Nubase long-term memory.',
|
|
27
|
+
inputSchema: objectSchema({
|
|
28
|
+
query: { type: 'string' },
|
|
29
|
+
topK: { type: 'number' },
|
|
30
|
+
userId: { type: 'string' },
|
|
31
|
+
agentId: { type: 'string' },
|
|
32
|
+
runId: { type: 'string' },
|
|
33
|
+
}, ['query']),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'memory_write',
|
|
37
|
+
description: 'Write durable Nubase memory.',
|
|
38
|
+
inputSchema: objectSchema({
|
|
39
|
+
content: { type: 'string' },
|
|
40
|
+
infer: { type: 'boolean' },
|
|
41
|
+
userId: { type: 'string' },
|
|
42
|
+
agentId: { type: 'string' },
|
|
43
|
+
runId: { type: 'string' },
|
|
44
|
+
}, ['content']),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'rest_select',
|
|
48
|
+
description: 'Call Nubase /rest/v1 for a table using a PostgREST query string, for example select=*&limit=10.',
|
|
49
|
+
inputSchema: objectSchema({
|
|
50
|
+
table: { type: 'string' },
|
|
51
|
+
query: { type: 'string' },
|
|
52
|
+
}, ['table']),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'sql_dry_run',
|
|
56
|
+
description: 'Classify SQL risk and statement count without executing it.',
|
|
57
|
+
inputSchema: objectSchema({ sql: { type: 'string' } }, ['sql']),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'sql_execute',
|
|
61
|
+
description: 'Execute SQL through Nubase admin API. Disabled unless NUBASE_ALLOW_SQL_EXECUTE=true.',
|
|
62
|
+
inputSchema: objectSchema({ sql: { type: 'string' } }, ['sql']),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
export async function callTool(name, args, config, client) {
|
|
66
|
+
switch (name) {
|
|
67
|
+
case 'nubase_capabilities':
|
|
68
|
+
return client.capabilities();
|
|
69
|
+
case 'nubase_instructions':
|
|
70
|
+
return client.instructions();
|
|
71
|
+
case 'memory_context':
|
|
72
|
+
return client.memoryContext(withScope(config, args));
|
|
73
|
+
case 'memory_search':
|
|
74
|
+
return client.memorySearch(withScope(config, args));
|
|
75
|
+
case 'memory_write':
|
|
76
|
+
return client.memoryWrite(withScope(config, args));
|
|
77
|
+
case 'rest_select':
|
|
78
|
+
return client.restSelect(args);
|
|
79
|
+
case 'sql_dry_run':
|
|
80
|
+
return client.sqlDryRun(args);
|
|
81
|
+
case 'sql_execute':
|
|
82
|
+
return client.sqlExecute(args);
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function objectSchema(properties, required = []) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties,
|
|
91
|
+
required,
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nubase_ai/mcp-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nubase-mcp-bridge": "dist/src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"test": "node --test dist/test/*.test.js",
|
|
18
|
+
"prepack": "pnpm run build",
|
|
19
|
+
"pack:check": "npm_config_cache=../../.npm-cache npm pack --dry-run"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.7.5",
|
|
26
|
+
"typescript": "^5.5.4"
|
|
27
|
+
}
|
|
28
|
+
}
|