@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 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
- "path": "npx",
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
  }
@@ -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
- * A server implementation that extends the base MCP server with telemetry capabilities.
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 {ServerOptions & { telemetry?: Telemetry }} [options] - Optional server configuration including telemetry
37
+ * @param {SfMcpServerOptions} [options] - Optional server configuration including telemetry and rate limiting
26
38
  */
27
- constructor(serverInfo: Implementation, options?: ServerOptions & {
28
- telemetry?: Telemetry;
29
- });
39
+ constructor(serverInfo: Implementation, options?: SfMcpServerOptions);
30
40
  connect: McpServer['connect'];
31
41
  tool: McpServer['tool'];
32
42
  }
@@ -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 {ServerOptions & { telemetry?: Telemetry }} [options] - Optional server configuration including telemetry
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;
@@ -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
- // Note: We could update ConfigAggregator to have a clearInstance method like StateAggregator
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.clearInstance();
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('@')) {