@salesforce/mcp 0.10.2 → 0.11.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/README.md +2 -4
- package/lib/sf-mcp-server.d.ts +15 -5
- package/lib/sf-mcp-server.js +32 -2
- package/lib/shared/auth.js +1 -3
- package/lib/shared/rate-limiter.d.ts +81 -0
- package/lib/shared/rate-limiter.js +130 -0
- package/lib/tools/users/sf-assign-permission-set.js +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -223,10 +223,8 @@ To configure [Cursor](https://www.cursor.com/) to work with Salesforce DX MCP Se
|
|
|
223
223
|
{
|
|
224
224
|
"mcpServers": {
|
|
225
225
|
"salesforce": {
|
|
226
|
-
"command":
|
|
227
|
-
|
|
228
|
-
"args": ["-y", "@salesforce/mcp", "--orgs", "DEFAULT_TARGET_ORG", "--toolsets", "all"]
|
|
229
|
-
}
|
|
226
|
+
"command": "npx",
|
|
227
|
+
"args": ["-y", "@salesforce/mcp", "--orgs", "DEFAULT_TARGET_ORG", "--toolsets", "all"]
|
|
230
228
|
}
|
|
231
229
|
}
|
|
232
230
|
}
|
package/lib/sf-mcp-server.d.ts
CHANGED
|
@@ -2,12 +2,22 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { Implementation } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { Telemetry } from './telemetry.js';
|
|
5
|
+
import { RateLimitConfig } from './shared/rate-limiter.js';
|
|
5
6
|
type ToolMethodSignatures = {
|
|
6
7
|
tool: McpServer['tool'];
|
|
7
8
|
connect: McpServer['connect'];
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Extended server options that include telemetry and rate limiting
|
|
12
|
+
*/
|
|
13
|
+
export type SfMcpServerOptions = ServerOptions & {
|
|
14
|
+
/** Optional telemetry instance for tracking server events */
|
|
15
|
+
telemetry?: Telemetry;
|
|
16
|
+
/** Optional rate limiting configuration */
|
|
17
|
+
rateLimit?: Partial<RateLimitConfig>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A server implementation that extends the base MCP server with telemetry and rate limiting capabilities.
|
|
11
21
|
*
|
|
12
22
|
* The method overloads for `tool` are taken directly from the source code for the original McpServer. They're
|
|
13
23
|
* copied here so that the types don't get lost.
|
|
@@ -18,15 +28,15 @@ export declare class SfMcpServer extends McpServer implements ToolMethodSignatur
|
|
|
18
28
|
private logger;
|
|
19
29
|
/** Optional telemetry instance for tracking server events */
|
|
20
30
|
private telemetry?;
|
|
31
|
+
/** Rate limiter for controlling tool call frequency */
|
|
32
|
+
private rateLimiter?;
|
|
21
33
|
/**
|
|
22
34
|
* Creates a new SfMcpServer instance
|
|
23
35
|
*
|
|
24
36
|
* @param {Implementation} serverInfo - The server implementation details
|
|
25
|
-
* @param {
|
|
37
|
+
* @param {SfMcpServerOptions} [options] - Optional server configuration including telemetry and rate limiting
|
|
26
38
|
*/
|
|
27
|
-
constructor(serverInfo: Implementation, options?:
|
|
28
|
-
telemetry?: Telemetry;
|
|
29
|
-
});
|
|
39
|
+
constructor(serverInfo: Implementation, options?: SfMcpServerOptions);
|
|
30
40
|
connect: McpServer['connect'];
|
|
31
41
|
tool: McpServer['tool'];
|
|
32
42
|
}
|
package/lib/sf-mcp-server.js
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
17
|
import { Logger } from '@salesforce/core';
|
|
18
|
+
import { createRateLimiter } from './shared/rate-limiter.js';
|
|
18
19
|
/**
|
|
19
|
-
* A server implementation that extends the base MCP server with telemetry capabilities.
|
|
20
|
+
* A server implementation that extends the base MCP server with telemetry and rate limiting capabilities.
|
|
20
21
|
*
|
|
21
22
|
* The method overloads for `tool` are taken directly from the source code for the original McpServer. They're
|
|
22
23
|
* copied here so that the types don't get lost.
|
|
@@ -27,15 +28,22 @@ export class SfMcpServer extends McpServer {
|
|
|
27
28
|
logger = Logger.childFromRoot('mcp-server');
|
|
28
29
|
/** Optional telemetry instance for tracking server events */
|
|
29
30
|
telemetry;
|
|
31
|
+
/** Rate limiter for controlling tool call frequency */
|
|
32
|
+
rateLimiter;
|
|
30
33
|
/**
|
|
31
34
|
* Creates a new SfMcpServer instance
|
|
32
35
|
*
|
|
33
36
|
* @param {Implementation} serverInfo - The server implementation details
|
|
34
|
-
* @param {
|
|
37
|
+
* @param {SfMcpServerOptions} [options] - Optional server configuration including telemetry and rate limiting
|
|
35
38
|
*/
|
|
36
39
|
constructor(serverInfo, options) {
|
|
37
40
|
super(serverInfo, options);
|
|
38
41
|
this.telemetry = options?.telemetry;
|
|
42
|
+
// Initialize rate limiter if configuration is provided
|
|
43
|
+
if (options?.rateLimit !== undefined) {
|
|
44
|
+
this.rateLimiter = createRateLimiter(options.rateLimit);
|
|
45
|
+
this.logger.debug('Rate limiter initialized', options.rateLimit);
|
|
46
|
+
}
|
|
39
47
|
this.server.oninitialized = () => {
|
|
40
48
|
const clientInfo = this.server.getClientVersion();
|
|
41
49
|
if (clientInfo) {
|
|
@@ -68,6 +76,28 @@ export class SfMcpServer extends McpServer {
|
|
|
68
76
|
const cb = rest[rest.length - 1];
|
|
69
77
|
const wrappedCb = async (args) => {
|
|
70
78
|
this.logger.debug(`Tool ${name} called`);
|
|
79
|
+
// Check rate limit before executing tool
|
|
80
|
+
if (this.rateLimiter) {
|
|
81
|
+
const rateLimitResult = this.rateLimiter.checkLimit();
|
|
82
|
+
if (!rateLimitResult.allowed) {
|
|
83
|
+
this.logger.warn(`Tool ${name} rate limited. Retry after: ${rateLimitResult.retryAfter ?? 0}ms`);
|
|
84
|
+
this.telemetry?.sendEvent('TOOL_RATE_LIMITED', {
|
|
85
|
+
name,
|
|
86
|
+
retryAfter: rateLimitResult.retryAfter,
|
|
87
|
+
remaining: rateLimitResult.remaining,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
isError: true,
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: `Rate limit exceeded. Too many tool calls. Please wait ${Math.ceil((rateLimitResult.retryAfter ?? 0) / 1000)} seconds before trying again.`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
this.logger.debug(`Tool ${name} rate check passed. Remaining: ${rateLimitResult.remaining}`);
|
|
100
|
+
}
|
|
71
101
|
const startTime = Date.now();
|
|
72
102
|
const result = await cb(args);
|
|
73
103
|
const runtimeMs = Date.now() - startTime;
|
package/lib/shared/auth.js
CHANGED
|
@@ -150,9 +150,7 @@ const defaultOrgMaps = {
|
|
|
150
150
|
async function getDefaultConfig(property) {
|
|
151
151
|
// If the directory changes, the singleton instance of ConfigAggregator is not updated.
|
|
152
152
|
// It continues to use the old local or global config.
|
|
153
|
-
|
|
154
|
-
// @ts-expect-error Accessing private static instance to reset singleton between directory changes
|
|
155
|
-
ConfigAggregator.instance = undefined;
|
|
153
|
+
await ConfigAggregator.clearInstance();
|
|
156
154
|
const aggregator = await ConfigAggregator.create();
|
|
157
155
|
const config = aggregator.getInfo(property);
|
|
158
156
|
const { value, path, key, location } = config;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for rate limiting
|
|
3
|
+
*/
|
|
4
|
+
export type RateLimitConfig = {
|
|
5
|
+
/** Maximum number of calls allowed per window (default: 60) */
|
|
6
|
+
limit: number;
|
|
7
|
+
/** Time window in milliseconds (default: 60000 = 1 minute) */
|
|
8
|
+
windowMs: number;
|
|
9
|
+
/** Allow burst of calls above the average rate (default: 10) */
|
|
10
|
+
burstAllowance: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Result of a rate limit check
|
|
14
|
+
*/
|
|
15
|
+
export type RateLimitResult = {
|
|
16
|
+
/** Whether the request is allowed */
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
/** Number of requests remaining in current window */
|
|
19
|
+
remaining: number;
|
|
20
|
+
/** Time in milliseconds until the window resets */
|
|
21
|
+
resetTime: number;
|
|
22
|
+
/** Time in milliseconds to wait before retrying (only set when allowed=false) */
|
|
23
|
+
retryAfter?: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Token bucket rate limiter with burst allowance support
|
|
27
|
+
*
|
|
28
|
+
* Uses a token bucket algorithm where:
|
|
29
|
+
* - Tokens are added at a steady rate (limit/windowMs)
|
|
30
|
+
* - Burst allowance determines the maximum tokens that can accumulate
|
|
31
|
+
* - Each request consumes one token
|
|
32
|
+
*/
|
|
33
|
+
export declare class RateLimiter {
|
|
34
|
+
private readonly config;
|
|
35
|
+
private tokens;
|
|
36
|
+
private lastRefill;
|
|
37
|
+
private readonly tokensPerMs;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new rate limiter
|
|
40
|
+
*
|
|
41
|
+
* @param config Rate limiting configuration
|
|
42
|
+
*/
|
|
43
|
+
constructor(config: RateLimitConfig);
|
|
44
|
+
/**
|
|
45
|
+
* Check if a request should be allowed and consume a token if so
|
|
46
|
+
*
|
|
47
|
+
* @returns Rate limit result
|
|
48
|
+
*/
|
|
49
|
+
checkLimit(): RateLimitResult;
|
|
50
|
+
/**
|
|
51
|
+
* Get current status without consuming a token
|
|
52
|
+
*
|
|
53
|
+
* @returns Rate limit result for status check
|
|
54
|
+
*/
|
|
55
|
+
getStatus(): RateLimitResult;
|
|
56
|
+
/**
|
|
57
|
+
* Refill tokens based on elapsed time
|
|
58
|
+
*
|
|
59
|
+
* @param now Current timestamp
|
|
60
|
+
*/
|
|
61
|
+
private refillTokens;
|
|
62
|
+
/**
|
|
63
|
+
* Calculate time until the rate limit window resets
|
|
64
|
+
*
|
|
65
|
+
* @returns Milliseconds until reset
|
|
66
|
+
*/
|
|
67
|
+
private calculateResetTime;
|
|
68
|
+
/**
|
|
69
|
+
* Calculate how long to wait before retrying
|
|
70
|
+
*
|
|
71
|
+
* @returns Milliseconds to wait
|
|
72
|
+
*/
|
|
73
|
+
private calculateRetryAfter;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create a rate limiter with default configuration
|
|
77
|
+
*
|
|
78
|
+
* @param overrides Partial configuration to override defaults
|
|
79
|
+
* @returns Configured rate limiter
|
|
80
|
+
*/
|
|
81
|
+
export declare function createRateLimiter(overrides?: Partial<RateLimitConfig>): RateLimiter;
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
/**
|
|
17
|
+
* Token bucket rate limiter with burst allowance support
|
|
18
|
+
*
|
|
19
|
+
* Uses a token bucket algorithm where:
|
|
20
|
+
* - Tokens are added at a steady rate (limit/windowMs)
|
|
21
|
+
* - Burst allowance determines the maximum tokens that can accumulate
|
|
22
|
+
* - Each request consumes one token
|
|
23
|
+
*/
|
|
24
|
+
export class RateLimiter {
|
|
25
|
+
config;
|
|
26
|
+
tokens;
|
|
27
|
+
lastRefill;
|
|
28
|
+
tokensPerMs;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new rate limiter
|
|
31
|
+
*
|
|
32
|
+
* @param config Rate limiting configuration
|
|
33
|
+
*/
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.tokens = config.burstAllowance; // Start with full burst capacity
|
|
37
|
+
this.lastRefill = Date.now();
|
|
38
|
+
this.tokensPerMs = config.limit / config.windowMs;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a request should be allowed and consume a token if so
|
|
42
|
+
*
|
|
43
|
+
* @returns Rate limit result
|
|
44
|
+
*/
|
|
45
|
+
checkLimit() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
this.refillTokens(now);
|
|
48
|
+
if (this.tokens >= 1) {
|
|
49
|
+
// Allow the request and consume a token
|
|
50
|
+
this.tokens -= 1;
|
|
51
|
+
return {
|
|
52
|
+
allowed: true,
|
|
53
|
+
remaining: Math.floor(this.tokens),
|
|
54
|
+
resetTime: this.calculateResetTime(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Rate limit exceeded
|
|
59
|
+
const retryAfter = this.calculateRetryAfter();
|
|
60
|
+
return {
|
|
61
|
+
allowed: false,
|
|
62
|
+
remaining: 0,
|
|
63
|
+
resetTime: this.calculateResetTime(),
|
|
64
|
+
retryAfter,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get current status without consuming a token
|
|
70
|
+
*
|
|
71
|
+
* @returns Rate limit result for status check
|
|
72
|
+
*/
|
|
73
|
+
getStatus() {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
this.refillTokens(now);
|
|
76
|
+
return {
|
|
77
|
+
allowed: this.tokens >= 1,
|
|
78
|
+
remaining: Math.floor(this.tokens),
|
|
79
|
+
resetTime: this.calculateResetTime(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Refill tokens based on elapsed time
|
|
84
|
+
*
|
|
85
|
+
* @param now Current timestamp
|
|
86
|
+
*/
|
|
87
|
+
refillTokens(now) {
|
|
88
|
+
const elapsed = now - this.lastRefill;
|
|
89
|
+
const tokensToAdd = elapsed * this.tokensPerMs;
|
|
90
|
+
if (tokensToAdd > 0) {
|
|
91
|
+
this.tokens = Math.min(this.config.burstAllowance, this.tokens + tokensToAdd);
|
|
92
|
+
this.lastRefill = now;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Calculate time until the rate limit window resets
|
|
97
|
+
*
|
|
98
|
+
* @returns Milliseconds until reset
|
|
99
|
+
*/
|
|
100
|
+
calculateResetTime() {
|
|
101
|
+
// Time to refill one token
|
|
102
|
+
const timePerToken = 1 / this.tokensPerMs;
|
|
103
|
+
return Math.ceil(timePerToken);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Calculate how long to wait before retrying
|
|
107
|
+
*
|
|
108
|
+
* @returns Milliseconds to wait
|
|
109
|
+
*/
|
|
110
|
+
calculateRetryAfter() {
|
|
111
|
+
// Time needed to accumulate one token
|
|
112
|
+
return Math.ceil(1 / this.tokensPerMs);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a rate limiter with default configuration
|
|
117
|
+
*
|
|
118
|
+
* @param overrides Partial configuration to override defaults
|
|
119
|
+
* @returns Configured rate limiter
|
|
120
|
+
*/
|
|
121
|
+
export function createRateLimiter(overrides = {}) {
|
|
122
|
+
const config = {
|
|
123
|
+
limit: 60, // 60 calls per minute
|
|
124
|
+
windowMs: 60 * 1000, // 1 minute
|
|
125
|
+
burstAllowance: 10, // Allow bursts of up to 10 calls
|
|
126
|
+
...overrides,
|
|
127
|
+
};
|
|
128
|
+
return new RateLimiter(config);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -69,7 +69,7 @@ export const registerToolAssignPermissionSet = (server) => {
|
|
|
69
69
|
const connection = await getConnection(usernameOrAlias);
|
|
70
70
|
// We need to clear the instance so we know we have the most up to date aliases
|
|
71
71
|
// If a user sets an alias after server start up, it was not getting picked up
|
|
72
|
-
StateAggregator.
|
|
72
|
+
await StateAggregator.clearInstanceAsync();
|
|
73
73
|
// Must NOT be nullish coalescing (??) In case the LLM uses and empty string
|
|
74
74
|
const assignTo = (await StateAggregator.getInstance()).aliases.resolveUsername(onBehalfOf || usernameOrAlias);
|
|
75
75
|
if (!assignTo.includes('@')) {
|