@gainable.dev/mcp-server 0.1.6 → 0.2.1
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/dist/collectionSchema.d.ts +5 -0
- package/dist/collectionSchema.js +23 -1
- package/dist/config.d.ts +18 -4
- package/dist/config.js +6 -9
- package/dist/gainableHttpRunner.d.ts +11 -6
- package/dist/gainableHttpRunner.js +62 -33
- package/dist/index.js +11 -21
- package/dist/scopedProvider.js +2 -1
- package/dist/scoping/resolveCollection.js +9 -8
- package/dist/stripUntrustedTags.d.ts +3 -1
- package/dist/stripUntrustedTags.js +8 -9
- package/package.json +1 -1
|
@@ -25,6 +25,11 @@ export declare function initSchemaInference(mongoUri: string, dbName: string): P
|
|
|
25
25
|
* This ensures synchronous cache lookups work from the very first query.
|
|
26
26
|
*/
|
|
27
27
|
export declare function preWarmSchemaCache(appName: string, allowedDatasets: string[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Lazy per-app schema warming. Only warms once per app per process lifetime.
|
|
30
|
+
* Called from createServerForRequest() so schemas are ready before the first query.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureSchemaCacheWarmed(appName: string, allowedDatasets: string[]): Promise<void>;
|
|
28
33
|
/**
|
|
29
34
|
* Infer schema from a collection by sampling documents.
|
|
30
35
|
* Results are cached for the lifetime of the process.
|
package/dist/collectionSchema.js
CHANGED
|
@@ -4,6 +4,10 @@ import { CORE_EXPOSED as CORE_COLLECTIONS } from './scoping/index.js';
|
|
|
4
4
|
* Cache of inferred schemas keyed by real collection name.
|
|
5
5
|
*/
|
|
6
6
|
const schemaCache = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Track which apps have already been warmed to avoid redundant work.
|
|
9
|
+
*/
|
|
10
|
+
const warmedApps = new Set();
|
|
7
11
|
/**
|
|
8
12
|
* Singleton DB connection for schema inference.
|
|
9
13
|
*/
|
|
@@ -29,32 +33,50 @@ export async function preWarmSchemaCache(appName, allowedDatasets) {
|
|
|
29
33
|
return;
|
|
30
34
|
try {
|
|
31
35
|
const collections = await schemaDb.listCollections().toArray();
|
|
36
|
+
const sizeBefore = schemaCache.size;
|
|
37
|
+
let matched = 0;
|
|
32
38
|
for (const col of collections) {
|
|
33
39
|
const name = col.name;
|
|
34
40
|
// App's custom collections
|
|
35
41
|
if (name.startsWith(`${appName}_`)) {
|
|
36
42
|
const cleanName = name.slice(appName.length + 1);
|
|
37
43
|
await getCollectionSchema(name, cleanName);
|
|
44
|
+
matched++;
|
|
38
45
|
continue;
|
|
39
46
|
}
|
|
40
47
|
// Allowed data collections
|
|
41
48
|
for (const datasetId of allowedDatasets) {
|
|
42
49
|
if (name === `data_${datasetId}`) {
|
|
43
50
|
await getCollectionSchema(name, datasetId);
|
|
51
|
+
matched++;
|
|
44
52
|
break;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
// Core collections (e.g. users)
|
|
48
56
|
if (CORE_COLLECTIONS.includes(name)) {
|
|
49
57
|
await getCollectionSchema(name, name);
|
|
58
|
+
matched++;
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
|
-
|
|
61
|
+
const cached = schemaCache.size - sizeBefore;
|
|
62
|
+
const skipped = matched - cached;
|
|
63
|
+
console.log(`[Schema] Pre-warmed schema for ${cached} collections${skipped > 0 ? ` (${skipped} empty collection${skipped > 1 ? 's' : ''} skipped)` : ''}`);
|
|
53
64
|
}
|
|
54
65
|
catch (err) {
|
|
55
66
|
console.warn('[Schema] Pre-warm failed:', err);
|
|
56
67
|
}
|
|
57
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Lazy per-app schema warming. Only warms once per app per process lifetime.
|
|
71
|
+
* Called from createServerForRequest() so schemas are ready before the first query.
|
|
72
|
+
*/
|
|
73
|
+
export async function ensureSchemaCacheWarmed(appName, allowedDatasets) {
|
|
74
|
+
const key = `${appName}:${allowedDatasets.sort().join(',')}`;
|
|
75
|
+
if (warmedApps.has(key))
|
|
76
|
+
return;
|
|
77
|
+
await preWarmSchemaCache(appName, allowedDatasets);
|
|
78
|
+
warmedApps.add(key);
|
|
79
|
+
}
|
|
58
80
|
/**
|
|
59
81
|
* Infer schema from a collection by sampling documents.
|
|
60
82
|
* Results are cached for the lifetime of the process.
|
package/dist/config.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Process-level config: loaded once at startup from env vars.
|
|
3
|
+
* Shared across all requests (one MCP process per account).
|
|
4
|
+
*/
|
|
5
|
+
export interface BaseConfig {
|
|
3
6
|
accountId: string;
|
|
4
|
-
allowedDatasets: string[];
|
|
5
7
|
mongodbUri: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Per-request config: extends BaseConfig with app + agent scoping.
|
|
11
|
+
* Built dynamically for each incoming MCP request.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScopingConfig extends BaseConfig {
|
|
14
|
+
appName: string;
|
|
15
|
+
allowedDatasets: string[];
|
|
6
16
|
/** Per-session field: which collections this agent can access. undefined = unrestricted. */
|
|
7
17
|
agentCollections?: string[];
|
|
8
18
|
}
|
|
9
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Load process-level config from environment variables.
|
|
21
|
+
* APP_NAME is no longer required at startup — it comes from the request query.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadBaseConfig(): BaseConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Load process-level config from environment variables.
|
|
3
|
+
* APP_NAME is no longer required at startup — it comes from the request query.
|
|
4
|
+
*/
|
|
5
|
+
export function loadBaseConfig() {
|
|
3
6
|
const accountId = process.env.ACCOUNT_ID;
|
|
4
7
|
const mongodbUri = process.env.MONGODB_URI;
|
|
5
|
-
if (!appName)
|
|
6
|
-
throw new Error('APP_NAME is required');
|
|
7
8
|
if (!accountId)
|
|
8
9
|
throw new Error('ACCOUNT_ID is required');
|
|
9
10
|
if (!mongodbUri)
|
|
10
11
|
throw new Error('MONGODB_URI is required');
|
|
11
|
-
|
|
12
|
-
.split(',')
|
|
13
|
-
.map(s => s.trim())
|
|
14
|
-
.filter(Boolean);
|
|
15
|
-
return { appName, accountId, allowedDatasets, mongodbUri };
|
|
12
|
+
return { accountId, mongodbUri };
|
|
16
13
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { StreamableHttpRunner } from 'mongodb-mcp-server';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BaseConfig } from './config.js';
|
|
3
3
|
/**
|
|
4
|
-
* Extends StreamableHttpRunner
|
|
4
|
+
* Extends StreamableHttpRunner for per-account MCP.
|
|
5
5
|
*
|
|
6
|
-
* Each
|
|
7
|
-
*
|
|
8
|
-
* which collections that agent is allowed to access (from MongoDB).
|
|
6
|
+
* Each request includes `?app={appName}&agent={agentId}` — both required.
|
|
7
|
+
* Attached data collections are read from `app_config` in the account DB per request.
|
|
9
8
|
*/
|
|
10
9
|
export declare class GainableHttpRunner extends StreamableHttpRunner {
|
|
11
10
|
private baseConfig;
|
|
12
11
|
private scopeClient;
|
|
13
|
-
constructor(config: ConstructorParameters<typeof StreamableHttpRunner>[0],
|
|
12
|
+
constructor(config: ConstructorParameters<typeof StreamableHttpRunner>[0], baseConfig: BaseConfig);
|
|
13
|
+
private getClient;
|
|
14
|
+
/**
|
|
15
|
+
* Look up attached data collections for an app from the app_config collection.
|
|
16
|
+
* Returns array of collection IDs (e.g. ["col_xxx", "col_yyy"]).
|
|
17
|
+
*/
|
|
18
|
+
private getAppDatasets;
|
|
14
19
|
/**
|
|
15
20
|
* Look up an agent's scopes from MongoDB.
|
|
16
21
|
* Returns the scopes array if the agent has restrictions, or undefined if unrestricted.
|
|
@@ -1,79 +1,108 @@
|
|
|
1
1
|
import { MongoClient } from 'mongodb';
|
|
2
2
|
import { StreamableHttpRunner } from 'mongodb-mcp-server';
|
|
3
3
|
import { createScopedConnectionManagerFactory } from './scopedConnectionManager.js';
|
|
4
|
+
import { ensureSchemaCacheWarmed } from './collectionSchema.js';
|
|
4
5
|
/**
|
|
5
|
-
* Extends StreamableHttpRunner
|
|
6
|
+
* Extends StreamableHttpRunner for per-account MCP.
|
|
6
7
|
*
|
|
7
|
-
* Each
|
|
8
|
-
*
|
|
9
|
-
* which collections that agent is allowed to access (from MongoDB).
|
|
8
|
+
* Each request includes `?app={appName}&agent={agentId}` — both required.
|
|
9
|
+
* Attached data collections are read from `app_config` in the account DB per request.
|
|
10
10
|
*/
|
|
11
11
|
export class GainableHttpRunner extends StreamableHttpRunner {
|
|
12
12
|
baseConfig;
|
|
13
13
|
scopeClient = null;
|
|
14
|
-
constructor(config,
|
|
14
|
+
constructor(config, baseConfig) {
|
|
15
15
|
super(config);
|
|
16
|
-
this.baseConfig =
|
|
16
|
+
this.baseConfig = baseConfig;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
* Look up an agent's scopes from MongoDB.
|
|
20
|
-
* Returns the scopes array if the agent has restrictions, or undefined if unrestricted.
|
|
21
|
-
*/
|
|
22
|
-
async getAgentScopes(agentUid) {
|
|
18
|
+
async getClient() {
|
|
23
19
|
if (!this.scopeClient) {
|
|
24
20
|
this.scopeClient = new MongoClient(this.baseConfig.mongodbUri);
|
|
25
21
|
await this.scopeClient.connect();
|
|
26
22
|
}
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
return this.scopeClient;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Look up attached data collections for an app from the app_config collection.
|
|
27
|
+
* Returns array of collection IDs (e.g. ["col_xxx", "col_yyy"]).
|
|
28
|
+
*/
|
|
29
|
+
async getAppDatasets(appName) {
|
|
30
|
+
const client = await this.getClient();
|
|
31
|
+
const db = client.db(this.baseConfig.accountId);
|
|
32
|
+
const config = await db.collection('app_config').findOne({ appId: appName }, { projection: { attachedCollections: 1 } });
|
|
33
|
+
return config?.attachedCollections || [];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Look up an agent's scopes from MongoDB.
|
|
37
|
+
* Returns the scopes array if the agent has restrictions, or undefined if unrestricted.
|
|
38
|
+
*/
|
|
39
|
+
async getAgentScopes(appName, agentUid) {
|
|
40
|
+
const client = await this.getClient();
|
|
41
|
+
const db = client.db(this.baseConfig.accountId);
|
|
42
|
+
const agent = await db.collection('agents').findOne({ appId: appName, uid: agentUid }, { projection: { scopes: 1 } });
|
|
29
43
|
if (!agent?.scopes || agent.scopes.length === 0)
|
|
30
44
|
return undefined;
|
|
31
45
|
// Normalize scopes: strip app prefix if present (e.g. "my-app_deals" → "deals")
|
|
32
|
-
const appPrefix = `${
|
|
46
|
+
const appPrefix = `${appName}_`;
|
|
33
47
|
return agent.scopes.map((s) => s.startsWith(appPrefix) ? s.slice(appPrefix.length) : s);
|
|
34
48
|
}
|
|
35
49
|
async createServerForRequest({ request, serverOptions, sessionOptions, }) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
// Both app and agent are required
|
|
51
|
+
const appName = extractQueryParam(request, 'app');
|
|
52
|
+
const agentId = extractQueryParam(request, 'agent');
|
|
53
|
+
if (!appName) {
|
|
54
|
+
throw new Error('Missing required query parameter: app');
|
|
55
|
+
}
|
|
56
|
+
if (!agentId) {
|
|
57
|
+
throw new Error('Missing required query parameter: agent');
|
|
40
58
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
// Look up attached collections from app_config in account DB
|
|
60
|
+
const allowedDatasets = await this.getAppDatasets(appName);
|
|
61
|
+
// Look up agent scopes
|
|
62
|
+
const agentCollections = await this.getAgentScopes(appName, agentId);
|
|
63
|
+
// Lazy schema warming for this app
|
|
64
|
+
await ensureSchemaCacheWarmed(appName, allowedDatasets);
|
|
65
|
+
console.log(`[MCP Session] app=${appName} | agent=${agentId} | datasets=${allowedDatasets.length > 0 ? allowedDatasets.join(',') : '(none)'} | scoped=${agentCollections ? 'yes' : 'no'}${agentCollections ? ` | collections=${JSON.stringify(agentCollections)}` : ''}`);
|
|
66
|
+
// Build per-request scoping config
|
|
67
|
+
const scopingConfig = {
|
|
44
68
|
...this.baseConfig,
|
|
69
|
+
appName,
|
|
70
|
+
allowedDatasets,
|
|
45
71
|
agentCollections,
|
|
46
72
|
};
|
|
47
|
-
// Create a per-session connection manager with
|
|
48
|
-
|
|
49
|
-
const connectionManager = await createScopedConnectionManagerFactory(agentConfig)({
|
|
73
|
+
// Create a per-session connection manager with request-specific scoping
|
|
74
|
+
const connectionManager = await createScopedConnectionManagerFactory(scopingConfig)({
|
|
50
75
|
logger: this.logger,
|
|
51
76
|
deviceId: this.deviceId,
|
|
52
77
|
userConfig: this.userConfig,
|
|
53
78
|
});
|
|
54
|
-
console.log('[MCP Session] Connection manager created');
|
|
55
|
-
console.log('[MCP Session] Creating server...');
|
|
56
79
|
const server = await this.createServer({
|
|
57
80
|
userConfig: this.userConfig,
|
|
58
81
|
logger: undefined,
|
|
59
82
|
serverOptions: {
|
|
60
83
|
tools: this.tools,
|
|
61
84
|
...serverOptions,
|
|
85
|
+
toolContext: {
|
|
86
|
+
appName,
|
|
87
|
+
accountId: this.baseConfig.accountId,
|
|
88
|
+
allowedDatasets,
|
|
89
|
+
},
|
|
62
90
|
},
|
|
63
91
|
sessionOptions: {
|
|
64
92
|
...sessionOptions,
|
|
65
93
|
connectionManager,
|
|
66
94
|
},
|
|
67
95
|
});
|
|
68
|
-
console.log('[MCP Session] Server created successfully');
|
|
69
96
|
return server;
|
|
70
97
|
}
|
|
71
98
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
return
|
|
99
|
+
function extractQueryParam(request, param) {
|
|
100
|
+
if (!param || !request?.query)
|
|
101
|
+
return undefined;
|
|
102
|
+
const value = request.query[param];
|
|
103
|
+
if (typeof value === 'string')
|
|
104
|
+
return value;
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return value[0];
|
|
78
107
|
return undefined;
|
|
79
108
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { UserConfigSchema } from 'mongodb-mcp-server';
|
|
3
3
|
import { FindTool, AggregateTool, CountTool, ExportTool, InsertManyTool, UpdateManyTool, DeleteManyTool, ListCollectionsTool, CollectionSchemaTool, CollectionIndexesTool, ExplainTool, } from 'mongodb-mcp-server/tools';
|
|
4
|
-
import {
|
|
4
|
+
import { loadBaseConfig } from './config.js';
|
|
5
5
|
import { GainableHttpRunner } from './gainableHttpRunner.js';
|
|
6
|
-
import { unwrapTools
|
|
7
|
-
import { initSchemaInference
|
|
8
|
-
const config =
|
|
6
|
+
import { unwrapTools } from './stripUntrustedTags.js';
|
|
7
|
+
import { initSchemaInference } from './collectionSchema.js';
|
|
8
|
+
const config = loadBaseConfig();
|
|
9
9
|
// Initialize schema inference connection (used for validation + response hints)
|
|
10
|
+
// Schema warming happens lazily per-app in createServerForRequest()
|
|
10
11
|
await initSchemaInference(config.mongodbUri, config.accountId);
|
|
11
|
-
|
|
12
|
-
// Set tool context so response wrappers can resolve collection names
|
|
13
|
-
setToolContext(config.appName, config.allowedDatasets);
|
|
12
|
+
const port = parseInt(process.env.PORT || process.env.HTTP_PORT || '3099', 10);
|
|
14
13
|
const userConfig = UserConfigSchema.parse({
|
|
15
14
|
transport: 'http',
|
|
16
|
-
httpPort:
|
|
17
|
-
httpHost: '
|
|
15
|
+
httpPort: port,
|
|
16
|
+
httpHost: '0.0.0.0',
|
|
18
17
|
connectionString: config.mongodbUri,
|
|
19
18
|
telemetry: 'disabled',
|
|
20
19
|
// 24h idle timeout — sessions are long-lived (Weavy agent conversations)
|
|
21
20
|
idleTimeoutMs: 86400000,
|
|
22
21
|
notificationTimeoutMs: 82800000,
|
|
23
22
|
// Weavy manages session IDs externally — accept any session ID without requiring initialize
|
|
24
|
-
// This prevents 404 "session not found" after sidecar restarts
|
|
25
23
|
externallyManagedSessions: true,
|
|
26
24
|
// Auth: validate X-Internal-Key header on every request (returns 403 if invalid)
|
|
27
25
|
httpHeaders: process.env.INTERNAL_API_KEY
|
|
@@ -46,14 +44,6 @@ const tools = unwrapTools([
|
|
|
46
44
|
ExplainTool,
|
|
47
45
|
]);
|
|
48
46
|
const runner = new GainableHttpRunner({ userConfig, tools }, config);
|
|
49
|
-
await runner.start(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
appName: config.appName,
|
|
53
|
-
accountId: config.accountId,
|
|
54
|
-
allowedDatasets: config.allowedDatasets,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
console.log(`Gainable MCP Server running on http://127.0.0.1:${process.env.HTTP_PORT || '3099'}/mcp`);
|
|
59
|
-
console.log(`App: ${config.appName} | DB: ${config.accountId}`);
|
|
47
|
+
await runner.start();
|
|
48
|
+
console.log(`Gainable MCP Server running on http://0.0.0.0:${port}/mcp`);
|
|
49
|
+
console.log(`Account: ${config.accountId} | Per-request scoping via ?app=&agent=`);
|
package/dist/scopedProvider.js
CHANGED
|
@@ -123,8 +123,9 @@ function filterAndRenameCollections(collections, appName, allowedDatasets, agent
|
|
|
123
123
|
// Everything else is hidden
|
|
124
124
|
}
|
|
125
125
|
// Agent-level restriction: filter to only allowed collections
|
|
126
|
+
// Core collections (users, userfieldmetadatas) are always included
|
|
126
127
|
if (agentCollections) {
|
|
127
|
-
return results.filter(col => agentCollections.includes(col.name));
|
|
128
|
+
return results.filter(col => col.type === 'core' || agentCollections.includes(col.name));
|
|
128
129
|
}
|
|
129
130
|
return results;
|
|
130
131
|
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
export const CORE_EXPOSED = ['users', 'userfieldmetadatas'];
|
|
2
2
|
const BLOCKED = ['agents', 'connections', 'emailtemplates', 'emaillogs', 'datamodels'];
|
|
3
3
|
export function resolveCollection(cleanName, appName, allowedDatasets, agentCollections) {
|
|
4
|
-
//
|
|
5
|
-
if (agentCollections && !agentCollections.includes(cleanName)) {
|
|
6
|
-
throw new Error(`Collection '${cleanName}' is not accessible`);
|
|
7
|
-
}
|
|
8
|
-
// 1. Core collections — name stays as-is
|
|
4
|
+
// 1. Core collections — always accessible regardless of agent scoping
|
|
9
5
|
if (CORE_EXPOSED.includes(cleanName)) {
|
|
10
6
|
return { realName: cleanName, type: 'core', writable: true };
|
|
11
7
|
}
|
|
12
|
-
// 2.
|
|
8
|
+
// 2. Agent-level restriction: if agent has a scoped list, check it
|
|
9
|
+
// (core collections above are always allowed)
|
|
10
|
+
if (agentCollections && !agentCollections.includes(cleanName)) {
|
|
11
|
+
throw new Error(`Collection '${cleanName}' is not accessible`);
|
|
12
|
+
}
|
|
13
|
+
// 3. Data collections — check allowed datasets
|
|
13
14
|
for (const datasetId of allowedDatasets) {
|
|
14
15
|
const realName = `data_${datasetId}`;
|
|
15
16
|
if (cleanName === realName || cleanName === datasetId) {
|
|
16
17
|
return { realName, type: 'data', writable: false };
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
//
|
|
20
|
+
// 4. Blocked core/system collections
|
|
20
21
|
if (BLOCKED.includes(cleanName)) {
|
|
21
22
|
throw new Error(`Collection '${cleanName}' is not accessible`);
|
|
22
23
|
}
|
|
23
|
-
//
|
|
24
|
+
// 5. Custom collections — add app prefix
|
|
24
25
|
const prefixed = `${appName}_${cleanName}`;
|
|
25
26
|
return { realName: prefixed, type: 'custom', writable: true };
|
|
26
27
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ToolClass } from 'mongodb-mcp-server/tools';
|
|
2
|
-
export declare function setToolContext(appName: string, allowedDatasets: string[]): void;
|
|
3
2
|
/**
|
|
4
3
|
* Strips the `<untrusted-user-data-UUID>` wrapper and security warnings
|
|
5
4
|
* from tool response text content.
|
|
@@ -11,6 +10,9 @@ export declare function setToolContext(appName: string, allowedDatasets: string[
|
|
|
11
10
|
*
|
|
12
11
|
* This creates a new tool class that extends the original and post-processes
|
|
13
12
|
* the invoke() result to remove the wrapping.
|
|
13
|
+
*
|
|
14
|
+
* Uses `this.toolContext` (set per-session by the MCP framework) to resolve
|
|
15
|
+
* collection names — no module-level globals needed.
|
|
14
16
|
*/
|
|
15
17
|
export declare function unwrapTool<T extends ToolClass>(ToolCtor: T): T;
|
|
16
18
|
/**
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { getCollectionSchema, formatSchemaHint } from './collectionSchema.js';
|
|
2
2
|
import { resolveCollection } from './scoping/index.js';
|
|
3
|
-
/** Set by index.ts at startup so tool wrappers can resolve collection names. */
|
|
4
|
-
let _appName = '';
|
|
5
|
-
let _allowedDatasets = [];
|
|
6
|
-
export function setToolContext(appName, allowedDatasets) {
|
|
7
|
-
_appName = appName;
|
|
8
|
-
_allowedDatasets = allowedDatasets;
|
|
9
|
-
}
|
|
10
3
|
/**
|
|
11
4
|
* Strips the `<untrusted-user-data-UUID>` wrapper and security warnings
|
|
12
5
|
* from tool response text content.
|
|
@@ -18,6 +11,9 @@ export function setToolContext(appName, allowedDatasets) {
|
|
|
18
11
|
*
|
|
19
12
|
* This creates a new tool class that extends the original and post-processes
|
|
20
13
|
* the invoke() result to remove the wrapping.
|
|
14
|
+
*
|
|
15
|
+
* Uses `this.toolContext` (set per-session by the MCP framework) to resolve
|
|
16
|
+
* collection names — no module-level globals needed.
|
|
21
17
|
*/
|
|
22
18
|
export function unwrapTool(ToolCtor) {
|
|
23
19
|
// Create a subclass that overrides invoke to strip tags
|
|
@@ -49,10 +45,13 @@ export function unwrapTool(ToolCtor) {
|
|
|
49
45
|
}
|
|
50
46
|
// Append schema hint for the target collection so the agent
|
|
51
47
|
// knows the correct field names, types, and valid values.
|
|
48
|
+
// Read appName and allowedDatasets from per-session toolContext.
|
|
52
49
|
const collection = args[0]?.collection;
|
|
53
|
-
|
|
50
|
+
const appName = this.toolContext?.appName;
|
|
51
|
+
const allowedDatasets = this.toolContext?.allowedDatasets ?? [];
|
|
52
|
+
if (collection && appName) {
|
|
54
53
|
try {
|
|
55
|
-
const resolved = resolveCollection(collection,
|
|
54
|
+
const resolved = resolveCollection(collection, appName, allowedDatasets);
|
|
56
55
|
const schema = await getCollectionSchema(resolved.realName, collection);
|
|
57
56
|
if (schema) {
|
|
58
57
|
textParts.push(formatSchemaHint(schema));
|
package/package.json
CHANGED