@salesforce/mcp 0.11.3 → 0.12.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 +6 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +34 -14
- package/lib/sf-mcp-server.d.ts +3 -0
- package/lib/sf-mcp-server.js +13 -1
- package/lib/shared/auth.js +1 -1
- package/lib/shared/cache.d.ts +27 -3
- package/lib/shared/cache.js +46 -6
- package/lib/shared/tools.d.ts +64 -0
- package/lib/shared/tools.js +156 -0
- package/lib/shared/types.d.ts +5 -0
- package/lib/tools/dynamic/index.d.ts +2 -0
- package/lib/tools/dynamic/index.js +18 -0
- package/lib/tools/dynamic/sf-enable-tool.d.ts +2 -0
- package/lib/tools/dynamic/sf-enable-tool.js +38 -0
- package/lib/tools/dynamic/sf-list-tools.d.ts +2 -0
- package/lib/tools/dynamic/sf-list-tools.js +31 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -174,6 +174,12 @@ This example shows how to enable the `data`, `orgs`, and `metadata` toolsets whe
|
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
#### Dynamic Tools (Experimental)
|
|
178
|
+
|
|
179
|
+
The `--dynamic-tools` flag enables dynamic tool discovery and loading. When this flag is set, the MCP server starts with a minimal set of core tools and will load new tools as the need arises. This is useful for reducing initial context size and improving LLM performance.
|
|
180
|
+
|
|
181
|
+
**NOTE:** This feature works in VSCode and Cline but may not work in other environments.
|
|
182
|
+
|
|
177
183
|
#### Core Toolset (always enabled)
|
|
178
184
|
|
|
179
185
|
Includes this tool:
|
package/lib/index.d.ts
CHANGED
|
@@ -4,10 +4,11 @@ export default class McpServerCommand extends Command {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static flags: {
|
|
6
6
|
orgs: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
toolsets: import("@oclif/core/interfaces").OptionFlag<("data" | "experimental" | "metadata" | "all" | "users" | "orgs" | "testing")[], import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
toolsets: import("@oclif/core/interfaces").OptionFlag<("data" | "experimental" | "metadata" | "all" | "users" | "orgs" | "testing")[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
version: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
9
9
|
'no-telemetry': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'dynamic-tools': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
static examples: {
|
|
13
14
|
description: string;
|
package/lib/index.js
CHANGED
|
@@ -22,10 +22,11 @@ import * as data from './tools/data/index.js';
|
|
|
22
22
|
import * as users from './tools/users/index.js';
|
|
23
23
|
import * as testing from './tools/testing/index.js';
|
|
24
24
|
import * as metadata from './tools/metadata/index.js';
|
|
25
|
+
import * as dynamic from './tools/dynamic/index.js';
|
|
25
26
|
import Cache from './shared/cache.js';
|
|
26
27
|
import { Telemetry } from './telemetry.js';
|
|
27
28
|
import { SfMcpServer } from './sf-mcp-server.js';
|
|
28
|
-
|
|
29
|
+
import { determineToolsetsToEnable, TOOLSETS } from './shared/tools.js';
|
|
29
30
|
/**
|
|
30
31
|
* Sanitizes an array of org usernames by replacing specific orgs with a placeholder.
|
|
31
32
|
* Special values (DEFAULT_TARGET_ORG, DEFAULT_TARGET_DEV_HUB, ALLOW_ALL_ORGS) are preserved.
|
|
@@ -76,12 +77,12 @@ You can also use special values to control access to orgs:
|
|
|
76
77
|
},
|
|
77
78
|
}),
|
|
78
79
|
toolsets: Flags.option({
|
|
79
|
-
options: TOOLSETS,
|
|
80
|
+
options: ['all', ...TOOLSETS],
|
|
80
81
|
char: 't',
|
|
81
82
|
summary: 'Toolset to enable',
|
|
82
83
|
multiple: true,
|
|
83
84
|
delimiter: ',',
|
|
84
|
-
|
|
85
|
+
exclusive: ['dynamic-toolsets'],
|
|
85
86
|
})(),
|
|
86
87
|
version: Flags.version(),
|
|
87
88
|
'no-telemetry': Flags.boolean({
|
|
@@ -90,6 +91,11 @@ You can also use special values to control access to orgs:
|
|
|
90
91
|
debug: Flags.boolean({
|
|
91
92
|
summary: 'Enable debug logging',
|
|
92
93
|
}),
|
|
94
|
+
'dynamic-tools': Flags.boolean({
|
|
95
|
+
summary: 'Enable dynamic toolsets',
|
|
96
|
+
char: 'd',
|
|
97
|
+
exclusive: ['toolsets'],
|
|
98
|
+
}),
|
|
93
99
|
};
|
|
94
100
|
static examples = [
|
|
95
101
|
{
|
|
@@ -110,7 +116,7 @@ You can also use special values to control access to orgs:
|
|
|
110
116
|
const { flags } = await this.parse(McpServerCommand);
|
|
111
117
|
if (!flags['no-telemetry']) {
|
|
112
118
|
this.telemetry = new Telemetry(this.config, {
|
|
113
|
-
toolsets: flags.toolsets.join(', '),
|
|
119
|
+
toolsets: (flags.toolsets ?? []).join(', '),
|
|
114
120
|
orgs: sanitizeOrgInput(flags.orgs),
|
|
115
121
|
});
|
|
116
122
|
await this.telemetry.start();
|
|
@@ -119,7 +125,7 @@ You can also use special values to control access to orgs:
|
|
|
119
125
|
this.telemetry?.stop();
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
|
-
Cache.
|
|
128
|
+
await Cache.safeSet('allowedOrgs', new Set(flags.orgs));
|
|
123
129
|
this.logToStderr(`Allowed orgs:\n${flags.orgs.map((org) => `- ${org}`).join('\n')}`);
|
|
124
130
|
const server = new SfMcpServer({
|
|
125
131
|
name: 'sf-mcp-server',
|
|
@@ -128,20 +134,34 @@ You can also use special values to control access to orgs:
|
|
|
128
134
|
resources: {},
|
|
129
135
|
tools: {},
|
|
130
136
|
},
|
|
131
|
-
}, {
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
}, {
|
|
138
|
+
telemetry: this.telemetry,
|
|
139
|
+
dynamicTools: flags['dynamic-tools'] ?? false,
|
|
140
|
+
});
|
|
141
|
+
const toolsetsToEnable = determineToolsetsToEnable(flags.toolsets ?? ['all'], flags['dynamic-tools'] ?? false);
|
|
134
142
|
// ************************
|
|
135
143
|
// CORE TOOLS (always on)
|
|
144
|
+
// If you're adding a new tool to the core toolset, you MUST add it to the `CORE_TOOLS` array in shared/tools.ts
|
|
145
|
+
// otherwise SfMcpServer will not register it.
|
|
146
|
+
//
|
|
147
|
+
// Long term, we will want to consider a more elegant solution for registering core tools.
|
|
136
148
|
// ************************
|
|
137
149
|
this.logToStderr('Registering core tools');
|
|
138
150
|
// get username
|
|
139
151
|
core.registerToolGetUsername(server);
|
|
140
152
|
core.registerToolResume(server);
|
|
153
|
+
// DYNAMIC TOOLSETS
|
|
154
|
+
// ************************
|
|
155
|
+
if (toolsetsToEnable.dynamic) {
|
|
156
|
+
this.logToStderr('Registering dynamic tools');
|
|
157
|
+
// Individual tool management
|
|
158
|
+
dynamic.registerToolEnableTool(server);
|
|
159
|
+
dynamic.registerToolListTools(server);
|
|
160
|
+
}
|
|
141
161
|
// ************************
|
|
142
162
|
// ORG TOOLS
|
|
143
163
|
// ************************
|
|
144
|
-
if (
|
|
164
|
+
if (toolsetsToEnable.orgs) {
|
|
145
165
|
this.logToStderr('Registering org tools');
|
|
146
166
|
// list all orgs
|
|
147
167
|
orgs.registerToolListAllOrgs(server);
|
|
@@ -149,7 +169,7 @@ You can also use special values to control access to orgs:
|
|
|
149
169
|
// ************************
|
|
150
170
|
// DATA TOOLS
|
|
151
171
|
// ************************
|
|
152
|
-
if (
|
|
172
|
+
if (toolsetsToEnable.data) {
|
|
153
173
|
this.logToStderr('Registering data tools');
|
|
154
174
|
// query org
|
|
155
175
|
data.registerToolQueryOrg(server);
|
|
@@ -157,7 +177,7 @@ You can also use special values to control access to orgs:
|
|
|
157
177
|
// ************************
|
|
158
178
|
// USER TOOLS
|
|
159
179
|
// ************************
|
|
160
|
-
if (
|
|
180
|
+
if (toolsetsToEnable.users) {
|
|
161
181
|
this.logToStderr('Registering user tools');
|
|
162
182
|
// assign permission set
|
|
163
183
|
users.registerToolAssignPermissionSet(server);
|
|
@@ -165,7 +185,7 @@ You can also use special values to control access to orgs:
|
|
|
165
185
|
// ************************
|
|
166
186
|
// testing TOOLS
|
|
167
187
|
// ************************
|
|
168
|
-
if (
|
|
188
|
+
if (toolsetsToEnable.testing) {
|
|
169
189
|
this.logToStderr('Registering testing tools');
|
|
170
190
|
testing.registerToolTestApex(server);
|
|
171
191
|
testing.registerToolTestAgent(server);
|
|
@@ -173,7 +193,7 @@ You can also use special values to control access to orgs:
|
|
|
173
193
|
// ************************
|
|
174
194
|
// METADATA TOOLS
|
|
175
195
|
// ************************
|
|
176
|
-
if (
|
|
196
|
+
if (toolsetsToEnable.metadata) {
|
|
177
197
|
this.logToStderr('Registering metadata tools');
|
|
178
198
|
// deploy metadata
|
|
179
199
|
metadata.registerToolDeployMetadata(server);
|
|
@@ -186,7 +206,7 @@ You can also use special values to control access to orgs:
|
|
|
186
206
|
// This toolset needs to be explicitly enabled ('all' will not include it)
|
|
187
207
|
// Tools don't need to be in an 'experimental' directory, only registered here
|
|
188
208
|
// ************************
|
|
189
|
-
if (
|
|
209
|
+
if (toolsetsToEnable.experimental) {
|
|
190
210
|
this.logToStderr('Registering experimental tools');
|
|
191
211
|
// Add any experimental tools here
|
|
192
212
|
}
|
package/lib/sf-mcp-server.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type SfMcpServerOptions = ServerOptions & {
|
|
|
15
15
|
telemetry?: Telemetry;
|
|
16
16
|
/** Optional rate limiting configuration */
|
|
17
17
|
rateLimit?: Partial<RateLimitConfig>;
|
|
18
|
+
/** Enable dynamic tool loading */
|
|
19
|
+
dynamicTools?: boolean;
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
20
22
|
* A server implementation that extends the base MCP server with telemetry and rate limiting capabilities.
|
|
@@ -28,6 +30,7 @@ export declare class SfMcpServer extends McpServer implements ToolMethodSignatur
|
|
|
28
30
|
private logger;
|
|
29
31
|
/** Optional telemetry instance for tracking server events */
|
|
30
32
|
private telemetry?;
|
|
33
|
+
private dynamicTools;
|
|
31
34
|
/** Rate limiter for controlling tool call frequency */
|
|
32
35
|
private rateLimiter?;
|
|
33
36
|
/**
|
package/lib/sf-mcp-server.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
17
|
import { Logger } from '@salesforce/core';
|
|
18
|
+
import { addTool, CORE_TOOLS } from './shared/tools.js';
|
|
18
19
|
import { createRateLimiter } from './shared/rate-limiter.js';
|
|
19
20
|
/**
|
|
20
21
|
* A server implementation that extends the base MCP server with telemetry and rate limiting capabilities.
|
|
@@ -28,6 +29,7 @@ export class SfMcpServer extends McpServer {
|
|
|
28
29
|
logger = Logger.childFromRoot('mcp-server');
|
|
29
30
|
/** Optional telemetry instance for tracking server events */
|
|
30
31
|
telemetry;
|
|
32
|
+
dynamicTools;
|
|
31
33
|
/** Rate limiter for controlling tool call frequency */
|
|
32
34
|
rateLimiter;
|
|
33
35
|
/**
|
|
@@ -39,6 +41,7 @@ export class SfMcpServer extends McpServer {
|
|
|
39
41
|
constructor(serverInfo, options) {
|
|
40
42
|
super(serverInfo, options);
|
|
41
43
|
this.telemetry = options?.telemetry;
|
|
44
|
+
this.dynamicTools = options?.dynamicTools ?? false;
|
|
42
45
|
// Initialize rate limiter if configuration is provided
|
|
43
46
|
if (options?.rateLimit !== undefined) {
|
|
44
47
|
this.rateLimiter = createRateLimiter(options.rateLimit);
|
|
@@ -112,7 +115,16 @@ export class SfMcpServer extends McpServer {
|
|
|
112
115
|
return result;
|
|
113
116
|
};
|
|
114
117
|
// @ts-expect-error because we no longer know what the type of rest is
|
|
115
|
-
|
|
118
|
+
const tool = super.tool(name, ...rest.slice(0, -1), wrappedCb);
|
|
119
|
+
if (this.dynamicTools) {
|
|
120
|
+
// Only disable the tool if it's not a core tool
|
|
121
|
+
if (!CORE_TOOLS.includes(name))
|
|
122
|
+
tool.disable();
|
|
123
|
+
addTool(tool, name).catch((error) => {
|
|
124
|
+
this.logger.error(`Failed to add tool ${name}:`, error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return tool;
|
|
116
128
|
};
|
|
117
129
|
}
|
|
118
130
|
//# sourceMappingURL=sf-mcp-server.js.map
|
package/lib/shared/auth.js
CHANGED
|
@@ -95,7 +95,7 @@ export async function getAllAllowedOrgs() {
|
|
|
95
95
|
const url = new URL(import.meta.url);
|
|
96
96
|
const params = url.searchParams.get('orgs');
|
|
97
97
|
const paramOrg = params ? params : undefined;
|
|
98
|
-
const orgAllowList = paramOrg ? new Set([paramOrg]) : Cache.
|
|
98
|
+
const orgAllowList = paramOrg ? new Set([paramOrg]) : (await Cache.safeGet('allowedOrgs')) ?? new Set();
|
|
99
99
|
// Get all orgs on the user's machine
|
|
100
100
|
const allOrgs = await AuthInfo.listAllAuthorizations();
|
|
101
101
|
// Sanitize the orgs to remove sensitive data
|
package/lib/shared/cache.d.ts
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
|
+
import { ToolInfo } from './types.js';
|
|
1
2
|
type CacheContents = {
|
|
2
3
|
allowedOrgs: Set<string>;
|
|
4
|
+
tools: ToolInfo[];
|
|
3
5
|
};
|
|
4
6
|
type ValueOf<T> = T[keyof T];
|
|
5
7
|
/**
|
|
6
|
-
* A
|
|
8
|
+
* A thread-safe cache providing generic Map operations with mutex protection.
|
|
9
|
+
* Offers atomic read, write, and update operations for concurrent access.
|
|
7
10
|
*/
|
|
8
11
|
export default class Cache extends Map<keyof CacheContents, ValueOf<CacheContents>> {
|
|
9
12
|
private static instance;
|
|
10
|
-
|
|
13
|
+
private static mutex;
|
|
14
|
+
private constructor();
|
|
15
|
+
/**
|
|
16
|
+
* Get the singleton instance of the Cache
|
|
17
|
+
* Creates a new instance if one doesn't exist
|
|
18
|
+
*
|
|
19
|
+
* @returns The singleton Cache instance
|
|
20
|
+
*/
|
|
11
21
|
static getInstance(): Cache;
|
|
12
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Thread-safe atomic update operation
|
|
24
|
+
* Allows safe read-modify-write operations with mutex protection
|
|
25
|
+
*/
|
|
26
|
+
static safeUpdate<K extends keyof CacheContents>(key: K, updateFn: (currentValue: CacheContents[K]) => CacheContents[K]): Promise<CacheContents[K]>;
|
|
27
|
+
/**
|
|
28
|
+
* Thread-safe atomic read operation
|
|
29
|
+
*/
|
|
30
|
+
static safeGet<K extends keyof CacheContents>(key: K): Promise<CacheContents[K]>;
|
|
31
|
+
/**
|
|
32
|
+
* Thread-safe atomic write operation
|
|
33
|
+
*/
|
|
34
|
+
static safeSet<K extends keyof CacheContents>(key: K, value: CacheContents[K]): Promise<void>;
|
|
35
|
+
get<K extends keyof CacheContents>(key: K): CacheContents[K];
|
|
36
|
+
private initialize;
|
|
13
37
|
}
|
|
14
38
|
export {};
|
package/lib/shared/cache.js
CHANGED
|
@@ -13,23 +13,63 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { Mutex } from '@salesforce/core';
|
|
16
17
|
/**
|
|
17
|
-
* A
|
|
18
|
+
* A thread-safe cache providing generic Map operations with mutex protection.
|
|
19
|
+
* Offers atomic read, write, and update operations for concurrent access.
|
|
18
20
|
*/
|
|
19
21
|
export default class Cache extends Map {
|
|
20
22
|
static instance;
|
|
23
|
+
// Mutex for thread-safe cache operations
|
|
24
|
+
static mutex = new Mutex();
|
|
21
25
|
constructor() {
|
|
22
26
|
super();
|
|
23
|
-
this.
|
|
27
|
+
this.initialize();
|
|
24
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the singleton instance of the Cache
|
|
31
|
+
* Creates a new instance if one doesn't exist
|
|
32
|
+
*
|
|
33
|
+
* @returns The singleton Cache instance
|
|
34
|
+
*/
|
|
25
35
|
static getInstance() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
return (Cache.instance ??= new Cache());
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thread-safe atomic update operation
|
|
40
|
+
* Allows safe read-modify-write operations with mutex protection
|
|
41
|
+
*/
|
|
42
|
+
static async safeUpdate(key, updateFn) {
|
|
43
|
+
const cache = Cache.getInstance();
|
|
44
|
+
return Cache.mutex.lock(() => {
|
|
45
|
+
const currentValue = cache.get(key);
|
|
46
|
+
const newValue = updateFn(currentValue);
|
|
47
|
+
cache.set(key, newValue);
|
|
48
|
+
return newValue;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Thread-safe atomic read operation
|
|
53
|
+
*/
|
|
54
|
+
static async safeGet(key) {
|
|
55
|
+
const cache = Cache.getInstance();
|
|
56
|
+
return Cache.mutex.lock(() => cache.get(key));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thread-safe atomic write operation
|
|
60
|
+
*/
|
|
61
|
+
static async safeSet(key, value) {
|
|
62
|
+
const cache = Cache.getInstance();
|
|
63
|
+
return Cache.mutex.lock(() => {
|
|
64
|
+
cache.set(key, value);
|
|
65
|
+
});
|
|
30
66
|
}
|
|
31
67
|
get(key) {
|
|
32
68
|
return super.get(key);
|
|
33
69
|
}
|
|
70
|
+
initialize() {
|
|
71
|
+
this.set('allowedOrgs', new Set());
|
|
72
|
+
this.set('tools', []);
|
|
73
|
+
}
|
|
34
74
|
}
|
|
35
75
|
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare const TOOLSETS: readonly ["orgs", "data", "users", "metadata", "testing", "experimental"];
|
|
3
|
+
type Toolset = (typeof TOOLSETS)[number];
|
|
4
|
+
export declare const CORE_TOOLS: string[];
|
|
5
|
+
/**
|
|
6
|
+
* Determines which toolsets should be enabled based on the provided toolsets array and dynamic tools flag.
|
|
7
|
+
*
|
|
8
|
+
* @param {Array<Toolset | 'all'>} toolsets - Array of toolsets to enable. Can include 'all' to enable all non-experimental toolsets.
|
|
9
|
+
* @param {boolean} dynamicTools - Flag indicating whether dynamic tools should be enabled. When true, only core and dynamic toolsets are enabled.
|
|
10
|
+
* @returns {Record<Toolset | 'dynamic' | 'core', boolean>} Object mapping each toolset to a boolean indicating whether it should be enabled.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Enable all toolsets except experimental
|
|
14
|
+
* determineToolsetsToEnable(['all'], false)
|
|
15
|
+
* // Returns: { core: true, data: true, dynamic: false, experimental: false, metadata: true, orgs: true, testing: true, users: true }
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Enable only dynamic tools
|
|
19
|
+
* determineToolsetsToEnable([], true)
|
|
20
|
+
* // Returns: { core: true, data: false, dynamic: true, experimental: false, metadata: false, orgs: false, testing: false, users: false }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Enable specific toolsets
|
|
24
|
+
* determineToolsetsToEnable(['data', 'users'], false)
|
|
25
|
+
* // Returns: { core: true, data: true, dynamic: false, experimental: false, metadata: false, orgs: false, testing: false, users: true }
|
|
26
|
+
*/
|
|
27
|
+
export declare function determineToolsetsToEnable(toolsets: Array<Toolset | 'all'>, dynamicTools: boolean): Record<Toolset | 'dynamic' | 'core', boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Add a tool to the cache
|
|
30
|
+
*/
|
|
31
|
+
export declare function addTool(tool: RegisteredTool, name: string): Promise<{
|
|
32
|
+
success: boolean;
|
|
33
|
+
message: string;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Enable an individual tool
|
|
37
|
+
*/
|
|
38
|
+
export declare function enableTool(toolName: string): Promise<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
message: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Disable an individual tool
|
|
44
|
+
*/
|
|
45
|
+
export declare function disableTool(toolName: string): Promise<{
|
|
46
|
+
success: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Get individual tool status
|
|
51
|
+
*/
|
|
52
|
+
export declare function getToolStatus(toolName: string): Promise<{
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
description: string;
|
|
55
|
+
} | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* List all individual tools with their status
|
|
58
|
+
*/
|
|
59
|
+
export declare function listAllTools(): Promise<Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
description: string;
|
|
63
|
+
}>>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025, Salesforce, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import Cache from './cache.js';
|
|
17
|
+
export const TOOLSETS = ['orgs', 'data', 'users', 'metadata', 'testing', 'experimental'];
|
|
18
|
+
/*
|
|
19
|
+
* These are tools that are always enabled at startup. They cannot be disabled and they cannot be dynamically enabled.
|
|
20
|
+
*/
|
|
21
|
+
export const CORE_TOOLS = ['sf-get-username', 'sf-resume', 'sf-enable-tool', 'sf-list-tools'];
|
|
22
|
+
/**
|
|
23
|
+
* Determines which toolsets should be enabled based on the provided toolsets array and dynamic tools flag.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<Toolset | 'all'>} toolsets - Array of toolsets to enable. Can include 'all' to enable all non-experimental toolsets.
|
|
26
|
+
* @param {boolean} dynamicTools - Flag indicating whether dynamic tools should be enabled. When true, only core and dynamic toolsets are enabled.
|
|
27
|
+
* @returns {Record<Toolset | 'dynamic' | 'core', boolean>} Object mapping each toolset to a boolean indicating whether it should be enabled.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Enable all toolsets except experimental
|
|
31
|
+
* determineToolsetsToEnable(['all'], false)
|
|
32
|
+
* // Returns: { core: true, data: true, dynamic: false, experimental: false, metadata: true, orgs: true, testing: true, users: true }
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Enable only dynamic tools
|
|
36
|
+
* determineToolsetsToEnable([], true)
|
|
37
|
+
* // Returns: { core: true, data: false, dynamic: true, experimental: false, metadata: false, orgs: false, testing: false, users: false }
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Enable specific toolsets
|
|
41
|
+
* determineToolsetsToEnable(['data', 'users'], false)
|
|
42
|
+
* // Returns: { core: true, data: true, dynamic: false, experimental: false, metadata: false, orgs: false, testing: false, users: true }
|
|
43
|
+
*/
|
|
44
|
+
export function determineToolsetsToEnable(toolsets, dynamicTools) {
|
|
45
|
+
if (dynamicTools) {
|
|
46
|
+
return {
|
|
47
|
+
core: true,
|
|
48
|
+
data: true,
|
|
49
|
+
dynamic: true,
|
|
50
|
+
experimental: false,
|
|
51
|
+
metadata: true,
|
|
52
|
+
orgs: true,
|
|
53
|
+
testing: true,
|
|
54
|
+
users: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (toolsets.includes('all')) {
|
|
58
|
+
return {
|
|
59
|
+
core: true,
|
|
60
|
+
data: true,
|
|
61
|
+
dynamic: false,
|
|
62
|
+
experimental: false,
|
|
63
|
+
metadata: true,
|
|
64
|
+
orgs: true,
|
|
65
|
+
testing: true,
|
|
66
|
+
users: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
core: true,
|
|
71
|
+
data: toolsets.includes('data'),
|
|
72
|
+
dynamic: false,
|
|
73
|
+
experimental: toolsets.includes('experimental'),
|
|
74
|
+
metadata: toolsets.includes('metadata'),
|
|
75
|
+
orgs: toolsets.includes('orgs'),
|
|
76
|
+
testing: toolsets.includes('testing'),
|
|
77
|
+
users: toolsets.includes('users'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Add a tool to the cache
|
|
82
|
+
*/
|
|
83
|
+
export async function addTool(tool, name) {
|
|
84
|
+
const existingTools = await Cache.safeGet('tools');
|
|
85
|
+
// Check if tool already exists
|
|
86
|
+
const existingTool = existingTools.find((t) => t.name === name);
|
|
87
|
+
if (existingTool) {
|
|
88
|
+
return { success: false, message: `Tool ${name} already exists` };
|
|
89
|
+
}
|
|
90
|
+
await Cache.safeUpdate('tools', (toolsArray) => {
|
|
91
|
+
const newTool = {
|
|
92
|
+
tool,
|
|
93
|
+
name,
|
|
94
|
+
};
|
|
95
|
+
return [...toolsArray, newTool];
|
|
96
|
+
});
|
|
97
|
+
return { success: true, message: `Added tool ${name}` };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Enable an individual tool
|
|
101
|
+
*/
|
|
102
|
+
export async function enableTool(toolName) {
|
|
103
|
+
const tools = await Cache.safeGet('tools');
|
|
104
|
+
const toolInfo = tools.find((t) => t.name === toolName);
|
|
105
|
+
if (!toolInfo) {
|
|
106
|
+
return { success: false, message: `Tool ${toolName} not found` };
|
|
107
|
+
}
|
|
108
|
+
if (toolInfo.tool.enabled) {
|
|
109
|
+
return { success: false, message: `Tool ${toolName} is already enabled` };
|
|
110
|
+
}
|
|
111
|
+
// Enable the tool directly
|
|
112
|
+
toolInfo.tool.enable();
|
|
113
|
+
return { success: true, message: `Tool ${toolName} enabled` };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Disable an individual tool
|
|
117
|
+
*/
|
|
118
|
+
export async function disableTool(toolName) {
|
|
119
|
+
const tools = await Cache.safeGet('tools');
|
|
120
|
+
const toolInfo = tools.find((t) => t.name === toolName);
|
|
121
|
+
if (!toolInfo) {
|
|
122
|
+
return { success: false, message: `Tool ${toolName} not found` };
|
|
123
|
+
}
|
|
124
|
+
if (!toolInfo.tool.enabled) {
|
|
125
|
+
return { success: false, message: `Tool ${toolName} is already disabled` };
|
|
126
|
+
}
|
|
127
|
+
// Disable the tool directly
|
|
128
|
+
toolInfo.tool.disable();
|
|
129
|
+
return { success: true, message: `Tool ${toolName} disabled` };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get individual tool status
|
|
133
|
+
*/
|
|
134
|
+
export async function getToolStatus(toolName) {
|
|
135
|
+
const tools = await Cache.safeGet('tools');
|
|
136
|
+
const toolInfo = tools.find((t) => t.name === toolName);
|
|
137
|
+
if (!toolInfo) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
enabled: toolInfo.tool.enabled,
|
|
142
|
+
description: toolInfo.tool.description ?? '',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* List all individual tools with their status
|
|
147
|
+
*/
|
|
148
|
+
export async function listAllTools() {
|
|
149
|
+
const tools = await Cache.safeGet('tools');
|
|
150
|
+
return tools.map((toolInfo) => ({
|
|
151
|
+
name: toolInfo.name,
|
|
152
|
+
enabled: toolInfo.tool.enabled,
|
|
153
|
+
description: toolInfo.tool.description ?? '',
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=tools.js.map
|
package/lib/shared/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigInfo } from '@salesforce/core';
|
|
2
2
|
import { type Nullable } from '@salesforce/ts-types';
|
|
3
|
+
import { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
export type ConfigInfoWithCache = {
|
|
4
5
|
key: string;
|
|
5
6
|
location?: ConfigInfo['location'];
|
|
@@ -26,3 +27,7 @@ export type ToolTextResponse = {
|
|
|
26
27
|
text: string;
|
|
27
28
|
}>;
|
|
28
29
|
};
|
|
30
|
+
export type ToolInfo = {
|
|
31
|
+
tool: RegisteredTool;
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025, Salesforce, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export { registerToolEnableTool } from './sf-enable-tool.js';
|
|
17
|
+
export { registerToolListTools } from './sf-list-tools.js';
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025, Salesforce, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { textResponse } from '../../shared/utils.js';
|
|
18
|
+
import { enableTool } from '../../shared/tools.js';
|
|
19
|
+
const enableToolParamsSchema = z.object({
|
|
20
|
+
tool: z.string().describe('The name of the tool to enable'),
|
|
21
|
+
});
|
|
22
|
+
export function registerToolEnableTool(server) {
|
|
23
|
+
server.tool('sf-enable-tool', `Enable one of the tools the Salesforce MCP server provides.
|
|
24
|
+
|
|
25
|
+
AGENT INSTRUCTIONS:
|
|
26
|
+
use sf-list-all-tools first to learn what tools are available for enabling'`, enableToolParamsSchema.shape, {
|
|
27
|
+
title: 'Enable an individual tool',
|
|
28
|
+
readOnlyHint: true,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
}, async ({ tool }) => {
|
|
31
|
+
const result = await enableTool(tool);
|
|
32
|
+
if (result.success) {
|
|
33
|
+
server.sendToolListChanged();
|
|
34
|
+
}
|
|
35
|
+
return textResponse(result.message, !result.success);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sf-enable-tool.js.map
|