@mastra/mcp 1.13.1-alpha.0 → 1.14.0-alpha.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/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
8
8
  import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
9
9
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
10
10
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
11
- import { LoggingMessageNotificationSchema, ListRootsRequestSchema, ListResourcesResultSchema, ReadResourceResultSchema, EmptyResultSchema, ListResourceTemplatesResultSchema, ListPromptsResultSchema, GetPromptResultSchema, PromptListChangedNotificationSchema, ResourceUpdatedNotificationSchema, ResourceListChangedNotificationSchema, ElicitRequestSchema, ProgressNotificationSchema, ListToolsRequestSchema, CallToolRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ErrorCode, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, ListPromptsRequestSchema, PromptSchema, GetPromptRequestSchema, CallToolResultSchema, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
11
+ import { LoggingMessageNotificationSchema, ListRootsRequestSchema, ListResourcesResultSchema, ReadResourceResultSchema, EmptyResultSchema, ListResourceTemplatesResultSchema, ListPromptsResultSchema, GetPromptResultSchema, PromptListChangedNotificationSchema, ToolListChangedNotificationSchema, ResourceUpdatedNotificationSchema, ResourceListChangedNotificationSchema, ElicitRequestSchema, ProgressNotificationSchema, ListToolsRequestSchema, CallToolRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ErrorCode, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, ListPromptsRequestSchema, PromptSchema, GetPromptRequestSchema, CallToolResultSchema, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
12
12
  import { asyncExitHook, gracefulExit } from 'exit-hook';
13
13
  import { createHash, randomUUID } from 'crypto';
14
14
  import equal from 'fast-deep-equal';
@@ -31484,6 +31484,7 @@ var InternalMastraMCPClient = class extends MastraBase {
31484
31484
  sigHupHandler;
31485
31485
  serverInstructions;
31486
31486
  _roots;
31487
+ hasElicitationCapability;
31487
31488
  requireToolApproval;
31488
31489
  onToolError;
31489
31490
  /** Provides access to resource operations (list, read, subscribe, etc.) */
@@ -31514,13 +31515,13 @@ var InternalMastraMCPClient = class extends MastraBase {
31514
31515
  this.requireToolApproval = server.requireToolApproval;
31515
31516
  this.onToolError = server.onToolError ?? "throw";
31516
31517
  this._roots = server.roots ?? [];
31518
+ this.hasElicitationCapability = capabilities.elicitation !== void 0;
31517
31519
  const hasRoots = this._roots.length > 0 || !!capabilities.roots;
31518
31520
  const clientCapabilities = {
31519
31521
  ...capabilities,
31520
- // Merge elicitation capabilities instead of overwriting
31521
- elicitation: {
31522
- ...capabilities.elicitation ?? {}
31523
- },
31522
+ // Only advertise elicitation when explicitly configured or when a handler
31523
+ // registers it before connect(). `elicitation: {}` is legacy form support.
31524
+ ...capabilities.elicitation !== void 0 ? { elicitation: { ...capabilities.elicitation } } : {},
31524
31525
  // Auto-enable roots capability if roots are provided
31525
31526
  ...hasRoots ? { roots: { listChanged: true, ...capabilities.roots ?? {} } } : {},
31526
31527
  // Advertise MCP Apps extension support so servers know we can render UI resources
@@ -31935,6 +31936,16 @@ var InternalMastraMCPClient = class extends MastraBase {
31935
31936
  handler();
31936
31937
  });
31937
31938
  }
31939
+ /**
31940
+ * Register a handler to be called when the tool list changes on the server.
31941
+ * Use this to re-fetch tools via `tools()` when notified.
31942
+ */
31943
+ setToolListChangedNotificationHandler(handler) {
31944
+ this.log("debug", "Setting tool list changed notification handler");
31945
+ this.client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
31946
+ handler();
31947
+ });
31948
+ }
31938
31949
  setResourceUpdatedNotificationHandler(handler) {
31939
31950
  this.log("debug", "Setting resource updated notification handler");
31940
31951
  this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, (notification) => {
@@ -31949,6 +31960,17 @@ var InternalMastraMCPClient = class extends MastraBase {
31949
31960
  }
31950
31961
  setElicitationRequestHandler(handler) {
31951
31962
  this.log("debug", "Setting elicitation request handler");
31963
+ if (!this.hasElicitationCapability) {
31964
+ try {
31965
+ this.client.registerCapabilities({ elicitation: { form: {} } });
31966
+ this.hasElicitationCapability = true;
31967
+ } catch (error51) {
31968
+ throw new Error(
31969
+ "Cannot register an elicitation handler after connecting unless elicitation capability was configured before initialization.",
31970
+ { cause: error51 }
31971
+ );
31972
+ }
31973
+ }
31952
31974
  this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
31953
31975
  this.log("debug", `Received elicitation request: ${request.params.message}`);
31954
31976
  return handler(request.params);
@@ -32411,7 +32433,7 @@ To fix this you have three different options:
32411
32433
  */
32412
32434
  onRequest: async (serverName, handler) => {
32413
32435
  try {
32414
- const internalClient = await this.getConnectedClientForServer(serverName);
32436
+ const internalClient = await this.getClientForServer(serverName);
32415
32437
  return internalClient.elicitation.onRequest(handler);
32416
32438
  } catch (err) {
32417
32439
  throw new MastraError(
@@ -32836,6 +32858,58 @@ To fix this you have three different options:
32836
32858
  }
32837
32859
  };
32838
32860
  }
32861
+ /**
32862
+ * Provides access to tool-related notification operations across all configured servers.
32863
+ *
32864
+ * To fetch tools, use `listTools()` or `listToolsets()`.
32865
+ *
32866
+ * @example
32867
+ * ```typescript
32868
+ * // React to tool list changes on a server
32869
+ * await mcp.tools.onListChanged('weatherServer', async () => {
32870
+ * console.log('Tool list changed, re-fetching...');
32871
+ * const tools = await mcp.listTools();
32872
+ * });
32873
+ * ```
32874
+ */
32875
+ get tools() {
32876
+ this.addToInstanceCache();
32877
+ return {
32878
+ /**
32879
+ * Sets a notification handler for when the tool list changes on a server.
32880
+ *
32881
+ * @param serverName - Name of the server to monitor
32882
+ * @param handler - Callback function invoked when tools are added/removed/modified
32883
+ * @returns Promise resolving when handler is registered
32884
+ * @throws {MastraError} If setting up the handler fails
32885
+ *
32886
+ * @example
32887
+ * ```typescript
32888
+ * await mcp.tools.onListChanged('weatherServer', async () => {
32889
+ * const tools = await mcp.listTools();
32890
+ * });
32891
+ * ```
32892
+ */
32893
+ onListChanged: async (serverName, handler) => {
32894
+ try {
32895
+ const internalClient = await this.getConnectedClientForServer(serverName);
32896
+ return internalClient.setToolListChangedNotificationHandler(handler);
32897
+ } catch (error51) {
32898
+ throw new MastraError(
32899
+ {
32900
+ id: "MCP_CLIENT_ON_LIST_CHANGED_TOOLS_FAILED",
32901
+ domain: ErrorDomain.MCP,
32902
+ category: ErrorCategory.THIRD_PARTY,
32903
+ details: {
32904
+ serverName
32905
+ }
32906
+ },
32907
+ error51
32908
+ );
32909
+ }
32910
+ }
32911
+ };
32912
+ }
32839
32913
  addToInstanceCache() {
32840
32914
  if (!mcpClientInstances.has(this.id)) {
32841
32915
  mcpClientInstances.set(this.id, this);
@@ -33127,7 +33201,14 @@ To fix this you have three different options:
33127
33201
  if (!client) return null;
33128
33202
  return client.stderr;
33129
33203
  }
33130
- async getConnectedClient(name, config2) {
33204
+ getServerConfig(serverName) {
33205
+ const serverConfig = this.serverConfigs[serverName];
33206
+ if (!serverConfig) {
33207
+ throw new Error(`Server configuration not found for name: ${serverName}`);
33208
+ }
33209
+ return serverConfig;
33210
+ }
33211
+ async getOrCreateClient(name, config2) {
33131
33212
  if (this.disconnectPromise) {
33132
33213
  await this.disconnectPromise;
33133
33214
  }
@@ -33138,10 +33219,8 @@ To fix this you have three different options:
33138
33219
  if (!existingClient) {
33139
33220
  throw new Error(`Client ${name} exists but is undefined`);
33140
33221
  }
33141
- await existingClient.connect();
33142
33222
  return existingClient;
33143
33223
  }
33144
- this.logger.debug("Connecting to MCP server", { name });
33145
33224
  const mcpClient = new InternalMastraMCPClient({
33146
33225
  name,
33147
33226
  server: config2,
@@ -33150,35 +33229,42 @@ To fix this you have three different options:
33150
33229
  });
33151
33230
  mcpClient.__setLogger(this.logger);
33152
33231
  this.mcpClientsById.set(name, mcpClient);
33232
+ return mcpClient;
33233
+ }
33234
+ async getConnectedClient(name, config2) {
33235
+ this.logger.debug("Connecting to MCP server", { name });
33236
+ const mcpClient = await this.getOrCreateClient(name, config2);
33153
33237
  try {
33154
33238
  await mcpClient.connect();
33155
33239
  } catch (e) {
33156
- const mastraError = new MastraError(
33157
- {
33158
- id: "MCP_CLIENT_CONNECT_FAILED",
33159
- domain: ErrorDomain.MCP,
33160
- category: ErrorCategory.THIRD_PARTY,
33161
- text: `Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
33162
- details: {
33163
- name
33164
- }
33165
- },
33166
- e
33167
- );
33168
- this.logger.trackException(mastraError);
33169
- this.logger.error("MCPClient errored connecting to MCP server:", { error: mastraError.toString() });
33170
- this.mcpClientsById.delete(name);
33171
- throw mastraError;
33240
+ throw this.handleConnectError(name, e);
33172
33241
  }
33173
33242
  this.logger.debug("Connected to MCP server", { name });
33174
33243
  return mcpClient;
33175
33244
  }
33245
+ handleConnectError(name, error51) {
33246
+ const mastraError = new MastraError(
33247
+ {
33248
+ id: "MCP_CLIENT_CONNECT_FAILED",
33249
+ domain: ErrorDomain.MCP,
33250
+ category: ErrorCategory.THIRD_PARTY,
33251
+ text: `Failed to connect to MCP server ${name}: ${error51 instanceof Error ? error51.stack || error51.message : String(error51)}`,
33252
+ details: {
33253
+ name
33254
+ }
33255
+ },
33256
+ error51
33257
+ );
33258
+ this.logger.trackException(mastraError);
33259
+ this.logger.error("MCPClient errored connecting to MCP server:", { error: mastraError.toString() });
33260
+ this.mcpClientsById.delete(name);
33261
+ return mastraError;
33262
+ }
33176
33263
  async getConnectedClientForServer(serverName) {
33177
- const serverConfig = this.serverConfigs[serverName];
33178
- if (!serverConfig) {
33179
- throw new Error(`Server configuration not found for name: ${serverName}`);
33180
- }
33181
- return this.getConnectedClient(serverName, serverConfig);
33264
+ return this.getConnectedClient(serverName, this.getServerConfig(serverName));
33265
+ }
33266
+ async getClientForServer(serverName) {
33267
+ return this.getOrCreateClient(serverName, this.getServerConfig(serverName));
33182
33268
  }
33183
33269
  async getToolsForServer(serverName) {
33184
33270
  for (let attempt = 1; attempt <= TOOL_DISCOVERY_MAX_ATTEMPTS; attempt++) {
@@ -33643,16 +33729,55 @@ var SSETransport = class {
33643
33729
  });
33644
33730
  }
33645
33731
  };
33732
+ async function broadcastNotification({
33733
+ servers,
33734
+ send,
33735
+ logger,
33736
+ errorId,
33737
+ errorText,
33738
+ errorDetails
33739
+ }) {
33740
+ const errors = [];
33741
+ for (const server of servers) {
33742
+ try {
33743
+ await send(server);
33744
+ } catch (error51) {
33745
+ errors.push(error51);
33746
+ }
33747
+ }
33748
+ if (errors.length === 0) return;
33749
+ const mastraError = new MastraError(
33750
+ {
33751
+ id: errorId,
33752
+ domain: ErrorDomain.MCP,
33753
+ category: ErrorCategory.THIRD_PARTY,
33754
+ text: errorText,
33755
+ details: {
33756
+ ...errorDetails,
33757
+ failedInstances: errors.length,
33758
+ totalInstances: servers.length
33759
+ }
33760
+ },
33761
+ errors[0]
33762
+ );
33763
+ logger.error(`${errorText}:`, { error: mastraError.toString() });
33764
+ if (errors.length === servers.length) {
33765
+ logger.trackException(mastraError);
33766
+ throw mastraError;
33767
+ }
33768
+ }
33769
+
33770
+ // src/server/promptActions.ts
33646
33771
  var ServerPromptActions = class {
33647
33772
  getLogger;
33648
- getSdkServer;
33773
+ getSdkServers;
33649
33774
  clearDefinedPrompts;
33650
33775
  /**
33651
33776
  * @internal
33652
33777
  */
33653
33778
  constructor(dependencies) {
33654
33779
  this.getLogger = dependencies.getLogger;
33655
- this.getSdkServer = dependencies.getSdkServer;
33780
+ this.getSdkServers = dependencies.getSdkServers;
33656
33781
  this.clearDefinedPrompts = dependencies.clearDefinedPrompts;
33657
33782
  }
33658
33783
  /**
@@ -33661,7 +33786,7 @@ var ServerPromptActions = class {
33661
33786
  * This clears the internal prompt cache and sends a `notifications/prompts/list_changed`
33662
33787
  * message to all clients, prompting them to re-fetch the prompt list.
33663
33788
  *
33664
- * @throws {MastraError} If sending the notification fails
33789
+ * @throws {MastraError} If sending the notification fails on all server instances
33665
33790
  *
33666
33791
  * @example
33667
33792
  * ```typescript
@@ -33672,47 +33797,39 @@ var ServerPromptActions = class {
33672
33797
  async notifyListChanged() {
33673
33798
  this.getLogger().info("Prompt list change externally notified. Clearing definedPrompts and sending notification.");
33674
33799
  this.clearDefinedPrompts();
33675
- try {
33676
- await this.getSdkServer().sendPromptListChanged();
33677
- } catch (error51) {
33678
- const mastraError = new MastraError(
33679
- {
33680
- id: "MCP_SERVER_PROMPT_LIST_CHANGED_NOTIFICATION_FAILED",
33681
- domain: ErrorDomain.MCP,
33682
- category: ErrorCategory.THIRD_PARTY,
33683
- text: "Failed to send prompt list changed notification"
33684
- },
33685
- error51
33686
- );
33687
- this.getLogger().error("Failed to send prompt list changed notification:", {
33688
- error: mastraError.toString()
33689
- });
33690
- this.getLogger().trackException(mastraError);
33691
- throw mastraError;
33692
- }
33800
+ await broadcastNotification({
33801
+ servers: this.getSdkServers(),
33802
+ send: (server) => server.sendPromptListChanged(),
33803
+ logger: this.getLogger(),
33804
+ errorId: "MCP_SERVER_PROMPT_LIST_CHANGED_NOTIFICATION_FAILED",
33805
+ errorText: "Failed to send prompt list changed notification"
33806
+ });
33693
33807
  }
33694
33808
  };
33809
+
33810
+ // src/server/resourceActions.ts
33695
33811
  var ServerResourceActions = class {
33696
- getSubscriptions;
33812
+ getSubscribedServers;
33697
33813
  getLogger;
33698
- getSdkServer;
33814
+ getSdkServers;
33699
33815
  /**
33700
33816
  * @internal
33701
33817
  */
33702
33818
  constructor(dependencies) {
33703
- this.getSubscriptions = dependencies.getSubscriptions;
33819
+ this.getSubscribedServers = dependencies.getSubscribedServers;
33704
33820
  this.getLogger = dependencies.getLogger;
33705
- this.getSdkServer = dependencies.getSdkServer;
33821
+ this.getSdkServers = dependencies.getSdkServers;
33706
33822
  }
33707
33823
  /**
33708
33824
  * Notifies subscribed clients that a specific resource has been updated.
33709
33825
  *
33710
- * If clients are subscribed to the resource URI, they will receive a
33711
- * `notifications/resources/updated` message to re-fetch the resource content.
33826
+ * Only clients that subscribed to the resource URI (via `resources/subscribe`)
33827
+ * receive a `notifications/resources/updated` message prompting them to
33828
+ * re-fetch the resource content.
33712
33829
  *
33713
33830
  * @param params - Notification parameters
33714
33831
  * @param params.uri - URI of the resource that was updated
33715
- * @throws {MastraError} If sending the notification fails
33832
+ * @throws {MastraError} If sending the notification fails on all subscribed server instances
33716
33833
  *
33717
33834
  * @example
33718
33835
  * ```typescript
@@ -33721,32 +33838,20 @@ var ServerResourceActions = class {
33721
33838
  * ```
33722
33839
  */
33723
33840
  async notifyUpdated({ uri }) {
33724
- if (this.getSubscriptions().has(uri)) {
33725
- this.getLogger().info(`Sending notifications/resources/updated for externally notified resource: ${uri}`);
33726
- try {
33727
- await this.getSdkServer().sendResourceUpdated({ uri });
33728
- } catch (error51) {
33729
- const mastraError = new MastraError(
33730
- {
33731
- id: "MCP_SERVER_RESOURCE_UPDATED_NOTIFICATION_FAILED",
33732
- domain: ErrorDomain.MCP,
33733
- category: ErrorCategory.THIRD_PARTY,
33734
- text: "Failed to send resource updated notification",
33735
- details: {
33736
- uri
33737
- }
33738
- },
33739
- error51
33740
- );
33741
- this.getLogger().trackException(mastraError);
33742
- this.getLogger().error("Failed to send resource updated notification:", {
33743
- error: mastraError.toString()
33744
- });
33745
- throw mastraError;
33746
- }
33747
- } else {
33841
+ const subscribedServers = this.getSubscribedServers(uri);
33842
+ if (subscribedServers.length === 0) {
33748
33843
  this.getLogger().debug(`Resource ${uri} was updated, but no active subscriptions for it.`);
33844
+ return;
33749
33845
  }
33846
+ this.getLogger().info(`Sending notifications/resources/updated for externally notified resource: ${uri}`);
33847
+ await broadcastNotification({
33848
+ servers: subscribedServers,
33849
+ send: (server) => server.sendResourceUpdated({ uri }),
33850
+ logger: this.getLogger(),
33851
+ errorId: "MCP_SERVER_RESOURCE_UPDATED_NOTIFICATION_FAILED",
33852
+ errorText: "Failed to send resource updated notification",
33853
+ errorDetails: { uri }
33854
+ });
33750
33855
  }
33751
33856
  /**
33752
33857
  * Notifies clients that the overall list of available resources has changed.
@@ -33755,7 +33860,7 @@ var ServerResourceActions = class {
33755
33860
  * them to re-fetch the resource list. Resource lists and templates are always evaluated
33756
33861
  * per request, so there is no server-side cache to clear.
33757
33862
  *
33758
- * @throws {MastraError} If sending the notification fails
33863
+ * @throws {MastraError} If sending the notification fails on all server instances
33759
33864
  *
33760
33865
  * @example
33761
33866
  * ```typescript
@@ -33765,28 +33870,110 @@ var ServerResourceActions = class {
33765
33870
  */
33766
33871
  async notifyListChanged() {
33767
33872
  this.getLogger().info("Resource list change externally notified. Sending notification.");
33768
- try {
33769
- await this.getSdkServer().sendResourceListChanged();
33770
- } catch (error51) {
33771
- const mastraError = new MastraError(
33772
- {
33773
- id: "MCP_SERVER_RESOURCE_LIST_CHANGED_NOTIFICATION_FAILED",
33774
- domain: ErrorDomain.MCP,
33775
- category: ErrorCategory.THIRD_PARTY,
33776
- text: "Failed to send resource list changed notification"
33777
- },
33778
- error51
33779
- );
33780
- this.getLogger().trackException(mastraError);
33781
- this.getLogger().error("Failed to send resource list changed notification:", {
33782
- error: mastraError.toString()
33783
- });
33784
- throw mastraError;
33873
+ await broadcastNotification({
33874
+ servers: this.getSdkServers(),
33875
+ send: (server) => server.sendResourceListChanged(),
33876
+ logger: this.getLogger(),
33877
+ errorId: "MCP_SERVER_RESOURCE_LIST_CHANGED_NOTIFICATION_FAILED",
33878
+ errorText: "Failed to send resource list changed notification"
33879
+ });
33880
+ }
33881
+ };
33882
+
33883
+ // src/server/toolActions.ts
33884
+ var ServerToolActions = class {
33885
+ getLogger;
33886
+ getSdkServers;
33887
+ addTools;
33888
+ removeTools;
33889
+ /**
33890
+ * @internal
33891
+ */
33892
+ constructor(dependencies) {
33893
+ this.getLogger = dependencies.getLogger;
33894
+ this.getSdkServers = dependencies.getSdkServers;
33895
+ this.addTools = dependencies.addTools;
33896
+ this.removeTools = dependencies.removeTools;
33897
+ }
33898
+ /**
33899
+ * Registers new tools on the running server and notifies connected clients
33900
+ * that the tool list has changed.
33901
+ *
33902
+ * Tools are keyed by their record key, the same as tools passed to the
33903
+ * `MCPServer` constructor. Adding a tool under an existing key replaces it.
33904
+ *
33905
+ * @param tools - Tools to register
33906
+ * @throws {MastraError} If sending the notification fails on all server instances
33907
+ *
33908
+ * @example
33909
+ * ```typescript
33910
+ * await server.toolActions.add({ myNewTool });
33911
+ * ```
33912
+ */
33913
+ async add(tools) {
33914
+ this.addTools(tools);
33915
+ await this.notifyListChanged();
33916
+ }
33917
+ /**
33918
+ * Removes tools from the running server and notifies connected clients that
33919
+ * the tool list has changed.
33920
+ *
33921
+ * Unknown tool IDs are ignored (logged as a warning). If no tools were
33922
+ * actually removed, no notification is sent.
33923
+ *
33924
+ * @param toolIds - IDs of the tools to remove
33925
+ * @throws {MastraError} If sending the notification fails on all server instances
33926
+ *
33927
+ * @example
33928
+ * ```typescript
33929
+ * await server.toolActions.remove(['myNewTool']);
33930
+ * ```
33931
+ */
33932
+ async remove(toolIds) {
33933
+ const removed = this.removeTools(toolIds);
33934
+ if (removed.length === 0) {
33935
+ this.getLogger().debug("No tools were removed; skipping tool list changed notification.");
33936
+ return;
33785
33937
  }
33938
+ await this.notifyListChanged();
33939
+ }
33940
+ /**
33941
+ * Notifies clients that the overall list of available tools has changed.
33942
+ *
33943
+ * This sends a `notifications/tools/list_changed` message to all clients,
33944
+ * prompting them to re-fetch the tool list.
33945
+ *
33946
+ * @throws {MastraError} If sending the notification fails on all server instances
33947
+ *
33948
+ * @example
33949
+ * ```typescript
33950
+ * // After changing which tools are available
33951
+ * await server.toolActions.notifyListChanged();
33952
+ * ```
33953
+ */
33954
+ async notifyListChanged() {
33955
+ this.getLogger().info("Tool list changed. Sending notification.");
33956
+ await broadcastNotification({
33957
+ servers: this.getSdkServers(),
33958
+ send: (server) => server.sendToolListChanged(),
33959
+ logger: this.getLogger(),
33960
+ errorId: "MCP_SERVER_TOOL_LIST_CHANGED_NOTIFICATION_FAILED",
33961
+ errorText: "Failed to send tool list changed notification"
33962
+ });
33786
33963
  }
33787
33964
  };
33788
33965
 
33789
33966
  // src/server/server.ts
33967
+ var LOG_LEVEL_SEVERITY = {
33968
+ debug: 0,
33969
+ info: 1,
33970
+ notice: 2,
33971
+ warning: 3,
33972
+ error: 4,
33973
+ critical: 5,
33974
+ alert: 6,
33975
+ emergency: 7
33976
+ };
33790
33977
  var MCPServer = class extends MCPServerBase {
33791
33978
  server;
33792
33979
  stdioTransport;
@@ -33805,8 +33992,12 @@ var MCPServer = class extends MCPServerBase {
33805
33992
  jsonSchemaValidator;
33806
33993
  mapAuthInfoToUser;
33807
33994
  fga;
33808
- subscriptions = /* @__PURE__ */ new Set();
33809
- currentLoggingLevel;
33995
+ // Resource subscriptions per server instance (main + per HTTP session), set via
33996
+ // resources/subscribe. Note: legacy SSE sessions share the main instance, so they
33997
+ // share one subscription set; streamable HTTP sessions are isolated per session.
33998
+ subscriptionsByInstance = /* @__PURE__ */ new WeakMap();
33999
+ // Minimum logging level per server instance (main + per HTTP session), set via logging/setLevel
34000
+ loggingLevels = /* @__PURE__ */ new WeakMap();
33810
34001
  /**
33811
34002
  * Provides methods to notify clients about resource changes.
33812
34003
  *
@@ -33830,6 +34021,24 @@ var MCPServer = class extends MCPServerBase {
33830
34021
  * ```
33831
34022
  */
33832
34023
  prompts;
34024
+ /**
34025
+ * Provides methods to dynamically manage tools and notify clients about
34026
+ * tool list changes. Named `toolActions` because `tools()` is the tool
34027
+ * registry getter inherited from `MCPServerBase`.
34028
+ *
34029
+ * @example
34030
+ * ```typescript
34031
+ * // Register a new tool at runtime and notify clients
34032
+ * await server.toolActions.add({ myNewTool });
34033
+ *
34034
+ * // Remove a tool and notify clients
34035
+ * await server.toolActions.remove(['myNewTool']);
34036
+ *
34037
+ * // Notify that the tool list changed (e.g. authorization changes)
34038
+ * await server.toolActions.notifyListChanged();
34039
+ * ```
34040
+ */
34041
+ toolActions;
33833
34042
  /**
33834
34043
  * Provides methods for interactive user input collection during tool execution.
33835
34044
  *
@@ -33947,7 +34156,7 @@ var MCPServer = class extends MCPServerBase {
33947
34156
  this.mapAuthInfoToUser = opts.mapAuthInfoToUser;
33948
34157
  this.fga = opts.fga;
33949
34158
  const capabilities = {
33950
- tools: {},
34159
+ tools: { listChanged: true },
33951
34160
  logging: { enabled: true }
33952
34161
  };
33953
34162
  if (this.resourceOptions) {
@@ -33986,23 +34195,144 @@ var MCPServer = class extends MCPServerBase {
33986
34195
  this.sseHonoTransports = /* @__PURE__ */ new Map();
33987
34196
  this.registerHandlersOnServer(this.server);
33988
34197
  this.resources = new ServerResourceActions({
33989
- getSubscriptions: () => this.subscriptions,
34198
+ getSubscribedServers: (uri) => this.getAllSdkServers().filter((server) => this.subscriptionsByInstance.get(server)?.has(uri)),
33990
34199
  getLogger: () => this.logger,
33991
- getSdkServer: () => this.server
34200
+ getSdkServers: () => this.getAllSdkServers()
33992
34201
  });
33993
34202
  this.prompts = new ServerPromptActions({
33994
34203
  getLogger: () => this.logger,
33995
- getSdkServer: () => this.server,
34204
+ getSdkServers: () => this.getAllSdkServers(),
33996
34205
  clearDefinedPrompts: () => {
33997
34206
  this.definedPrompts = void 0;
33998
34207
  }
33999
34208
  });
34209
+ this.toolActions = new ServerToolActions({
34210
+ getLogger: () => this.logger,
34211
+ getSdkServers: () => this.getAllSdkServers(),
34212
+ addTools: (tools) => this.addTools(tools),
34213
+ removeTools: (toolIds) => this.removeTools(toolIds)
34214
+ });
34000
34215
  this.elicitation = {
34001
34216
  sendRequest: async (request, options) => {
34002
34217
  return this.handleElicitationRequest(request, void 0, options);
34003
34218
  }
34004
34219
  };
34005
34220
  }
34221
+ /**
34222
+ * Returns every connected SDK server instance: the main instance (stdio/SSE
34223
+ * transports) plus one instance per streamable HTTP session. Used to
34224
+ * broadcast notifications to all connected clients. Instances without a
34225
+ * connected transport are skipped.
34226
+ *
34227
+ * Note: stateless/serverless requests use transient server instances and
34228
+ * cannot receive notifications.
34229
+ */
34230
+ getAllSdkServers() {
34231
+ return [this.server, ...this.httpServerInstances.values()].filter((server) => server.transport !== void 0);
34232
+ }
34233
+ /**
34234
+ * Determines whether a log message at the given level should be sent to the
34235
+ * client connected to the given server instance, honoring the minimum level
34236
+ * the client set via `logging/setLevel` (RFC 5424 severity ordering).
34237
+ * When the client never set a level, all messages are sent.
34238
+ */
34239
+ shouldSendLog(serverInstance, level) {
34240
+ const minimumLevel = this.loggingLevels.get(serverInstance);
34241
+ if (minimumLevel === void 0) return true;
34242
+ return LOG_LEVEL_SEVERITY[level] >= LOG_LEVEL_SEVERITY[minimumLevel];
34243
+ }
34244
+ /**
34245
+ * Sends a `notifications/message` log notification to connected clients.
34246
+ *
34247
+ * The notification is broadcast to every active server instance, honoring
34248
+ * the minimum logging level each client set via `logging/setLevel`.
34249
+ *
34250
+ * @param params - Log message parameters
34251
+ * @param params.level - Log severity level
34252
+ * @param params.data - Arbitrary JSON-serializable data to log
34253
+ * @param params.logger - Optional logger name
34254
+ * @throws {MastraError} If sending the notification fails on all eligible server instances
34255
+ *
34256
+ * @example
34257
+ * ```typescript
34258
+ * await server.sendLoggingMessage({
34259
+ * level: 'info',
34260
+ * data: { message: 'Sync completed', itemsProcessed: 42 },
34261
+ * });
34262
+ * ```
34263
+ */
34264
+ async sendLoggingMessage(params) {
34265
+ const eligibleServers = this.getAllSdkServers().filter((server) => this.shouldSendLog(server, params.level));
34266
+ if (eligibleServers.length === 0) {
34267
+ this.logger.debug("No eligible clients for log message; skipping.", { level: params.level });
34268
+ return;
34269
+ }
34270
+ await broadcastNotification({
34271
+ servers: eligibleServers,
34272
+ send: (server) => server.sendLoggingMessage(params),
34273
+ logger: this.logger,
34274
+ errorId: "MCP_SERVER_LOGGING_MESSAGE_NOTIFICATION_FAILED",
34275
+ errorText: "Failed to send logging message notification",
34276
+ errorDetails: { level: params.level }
34277
+ });
34278
+ }
34279
+ /**
34280
+ * Registers new tools on the running server. Tools are merged into both the
34281
+ * converted tool registry (used by list/call handlers) and the original
34282
+ * tools config so they survive tool re-conversion when the server is
34283
+ * registered with a Mastra instance.
34284
+ */
34285
+ addTools(tools) {
34286
+ const converted = this.convertTools(tools);
34287
+ for (const key of Object.keys(converted)) {
34288
+ if (this.convertedTools[key]) {
34289
+ this.logger.warn(`Tool '${key}' already exists and will be replaced.`);
34290
+ }
34291
+ }
34292
+ this.convertedTools = { ...this.convertedTools, ...converted };
34293
+ this.originalTools = { ...this.originalTools, ...tools };
34294
+ if (this.mastra) {
34295
+ for (const [key, tool] of Object.entries(tools)) {
34296
+ if (tool && typeof tool === "object" && "id" in tool) {
34297
+ this.mastra.addTool(tool, this.mastraToolKey(key, tool));
34298
+ }
34299
+ }
34300
+ }
34301
+ }
34302
+ /**
34303
+ * Removes tools from the running server by tool ID.
34304
+ *
34305
+ * @returns The IDs of the tools that were actually removed
34306
+ */
34307
+ removeTools(toolIds) {
34308
+ const removed = [];
34309
+ const convertedTools = { ...this.convertedTools };
34310
+ const originalTools = { ...this.originalTools };
34311
+ for (const toolId of toolIds) {
34312
+ if (convertedTools[toolId]) {
34313
+ const originalTool = originalTools[toolId];
34314
+ delete convertedTools[toolId];
34315
+ delete originalTools[toolId];
34316
+ removed.push(toolId);
34317
+ if (this.mastra && originalTool && typeof originalTool === "object" && "id" in originalTool) {
34318
+ this.mastra.removeTool(this.mastraToolKey(toolId, originalTool));
34319
+ }
34320
+ } else {
34321
+ this.logger.warn(`Cannot remove tool '${toolId}': tool not found.`);
34322
+ }
34323
+ }
34324
+ this.convertedTools = convertedTools;
34325
+ this.originalTools = originalTools;
34326
+ return removed;
34327
+ }
34328
+ /**
34329
+ * The key a tool is registered under in the Mastra instance's tool
34330
+ * registry: the tool's intrinsic ID when present (avoids collisions across
34331
+ * MCP servers), falling back to its record key. Mirrors __registerMastra.
34332
+ */
34333
+ mastraToolKey(key, tool) {
34334
+ return "id" in tool && typeof tool.id === "string" ? tool.id : key;
34335
+ }
34006
34336
  /**
34007
34337
  * Handle an elicitation request by sending it to the connected client.
34008
34338
  * This method sends an elicitation/create request to the client and waits for the response.
@@ -34135,7 +34465,7 @@ var MCPServer = class extends MCPServerBase {
34135
34465
  */
34136
34466
  createServerInstance() {
34137
34467
  const capabilities = {
34138
- tools: {},
34468
+ tools: { listChanged: true },
34139
34469
  logging: { enabled: true }
34140
34470
  };
34141
34471
  if (this.resourceOptions) {
@@ -34254,6 +34584,33 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34254
34584
  }
34255
34585
  };
34256
34586
  const proxiedContext = await this.createProxiedRequestContext(extra);
34587
+ const sessionLog = async (level, message, data) => {
34588
+ if (!this.shouldSendLog(serverInstance, level)) return;
34589
+ await extra.sendNotification({
34590
+ method: "notifications/message",
34591
+ params: {
34592
+ level,
34593
+ logger: this.name,
34594
+ data: { message, ...data }
34595
+ }
34596
+ });
34597
+ };
34598
+ const progressToken = extra._meta?.progressToken;
34599
+ const sessionProgress = async (params) => {
34600
+ if (progressToken === void 0) {
34601
+ this.logger.debug("Tool attempted to send progress but the caller sent no progressToken; skipping.", {
34602
+ tool: request.params.name
34603
+ });
34604
+ return;
34605
+ }
34606
+ await extra.sendNotification({
34607
+ method: "notifications/progress",
34608
+ params: {
34609
+ progressToken,
34610
+ ...params
34611
+ }
34612
+ });
34613
+ };
34257
34614
  const mcpOptions = {
34258
34615
  messages: [],
34259
34616
  toolCallId: "",
@@ -34261,7 +34618,9 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34261
34618
  // Pass MCP-specific context through the mcp property
34262
34619
  mcp: {
34263
34620
  elicitation: sessionElicitation,
34264
- extra
34621
+ extra,
34622
+ log: sessionLog,
34623
+ progress: sessionProgress
34265
34624
  },
34266
34625
  // @ts-expect-error this is to let people know that the elicitation and extra keys are now nested under mcp.elicitation and mcp.extra in tool arguments
34267
34626
  get elicitation() {
@@ -34349,7 +34708,7 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34349
34708
  }
34350
34709
  });
34351
34710
  serverInstance.setRequestHandler(SetLevelRequestSchema, async (request) => {
34352
- this.currentLoggingLevel = request.params.level;
34711
+ this.loggingLevels.set(serverInstance, request.params.level);
34353
34712
  this.logger.debug("Logging level set", { level: request.params.level });
34354
34713
  return {};
34355
34714
  });
@@ -34393,11 +34752,13 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34393
34752
  try {
34394
34753
  const resourcesOrResourceContent = await capturedResourceOptions.getResourceContent({ uri, extra });
34395
34754
  const resourcesContent = Array.isArray(resourcesOrResourceContent) ? resourcesOrResourceContent : [resourcesOrResourceContent];
34755
+ const resourceMeta = resource._meta ? { _meta: resource._meta } : {};
34396
34756
  const contents = resourcesContent.map((resourceContent) => {
34397
34757
  if ("text" in resourceContent && resourceContent.text !== void 0) {
34398
34758
  return {
34399
34759
  uri: resource.uri,
34400
34760
  mimeType: resource.mimeType,
34761
+ ...resourceMeta,
34401
34762
  text: resourceContent.text
34402
34763
  };
34403
34764
  }
@@ -34408,6 +34769,7 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34408
34769
  return {
34409
34770
  uri: resource.uri,
34410
34771
  mimeType: resource.mimeType,
34772
+ ...resourceMeta,
34411
34773
  blob
34412
34774
  };
34413
34775
  });
@@ -34438,13 +34800,18 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
34438
34800
  serverInstance.setRequestHandler(SubscribeRequestSchema, async (request) => {
34439
34801
  const uri = request.params.uri;
34440
34802
  this.logger.info("Received resources/subscribe request", { uri });
34441
- this.subscriptions.add(uri);
34803
+ let subscriptions = this.subscriptionsByInstance.get(serverInstance);
34804
+ if (!subscriptions) {
34805
+ subscriptions = /* @__PURE__ */ new Set();
34806
+ this.subscriptionsByInstance.set(serverInstance, subscriptions);
34807
+ }
34808
+ subscriptions.add(uri);
34442
34809
  return {};
34443
34810
  });
34444
34811
  serverInstance.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
34445
34812
  const uri = request.params.uri;
34446
34813
  this.logger.info("Received resources/unsubscribe request", { uri });
34447
- this.subscriptions.delete(uri);
34814
+ this.subscriptionsByInstance.get(serverInstance)?.delete(uri);
34448
34815
  return {};
34449
34816
  });
34450
34817
  }
@@ -35014,7 +35381,8 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
35014
35381
  const isStatelessMode = options?.serverless || options && "sessionIdGenerator" in options && options.sessionIdGenerator === void 0;
35015
35382
  if (isStatelessMode) {
35016
35383
  this.logger.debug("Running in stateless mode");
35017
- await this.handleServerlessRequest(req, res);
35384
+ const enableJsonResponse = options?.enableJsonResponse ?? !options?.serverlessStreaming;
35385
+ await this.handleServerlessRequest(req, res, { enableJsonResponse });
35018
35386
  return;
35019
35387
  }
35020
35388
  const mergedOptions = {
@@ -35058,11 +35426,14 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
35058
35426
  const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
35059
35427
  if (isInitializeRequest(body)) {
35060
35428
  this.logger.debug("Received initialize request, creating new transport");
35429
+ const sessionServerInstance = this.createServerInstance();
35061
35430
  transport = new StreamableHTTPServerTransport({
35062
35431
  ...mergedOptions,
35063
35432
  sessionIdGenerator: mergedOptions.sessionIdGenerator,
35064
35433
  onsessioninitialized: (id) => {
35065
35434
  this.streamableHTTPTransports.set(id, transport);
35435
+ this.httpServerInstances.set(id, sessionServerInstance);
35436
+ this.logger.debug("Session initialized and stored", { sessionId: id });
35066
35437
  }
35067
35438
  });
35068
35439
  transport.onclose = () => {
@@ -35076,15 +35447,7 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
35076
35447
  }
35077
35448
  }
35078
35449
  };
35079
- const sessionServerInstance = this.createServerInstance();
35080
35450
  await sessionServerInstance.connect(transport);
35081
- if (transport.sessionId) {
35082
- this.streamableHTTPTransports.set(transport.sessionId, transport);
35083
- this.httpServerInstances.set(transport.sessionId, sessionServerInstance);
35084
- this.logger.debug("Session initialized and stored", { sessionId: transport.sessionId });
35085
- } else {
35086
- this.logger.warn("Transport initialized without a session ID");
35087
- }
35088
35451
  return await transport.handleRequest(req, res, body);
35089
35452
  } else {
35090
35453
  this.logger.warn("Received non-initialize POST request without session ID");
@@ -35155,21 +35518,26 @@ Provided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`
35155
35518
  *
35156
35519
  * @param req - Incoming HTTP request
35157
35520
  * @param res - HTTP response object
35521
+ * @param options - Transport options for this request
35522
+ * @param options.enableJsonResponse - When `true` (default), buffers and returns a single
35523
+ * JSON-RPC response. When `false`, the request is handled with request-scoped SSE streaming,
35524
+ * so in-request `notifications/progress` reach the client before the final result.
35158
35525
  * @private
35159
35526
  */
35160
- async handleServerlessRequest(req, res) {
35527
+ async handleServerlessRequest(req, res, { enableJsonResponse = true } = {}) {
35161
35528
  try {
35162
35529
  this.logger.debug("Received serverless request", { method: req.method });
35163
35530
  const body = req.method === "POST" ? await this.readJsonBody(req) : void 0;
35164
35531
  this.logger.debug("Processing serverless request", {
35165
35532
  method: req.method,
35166
35533
  bodyMethod: body?.method,
35167
- id: body?.id
35534
+ id: body?.id,
35535
+ enableJsonResponse
35168
35536
  });
35169
35537
  const transientServer = this.createServerInstance();
35170
35538
  const tempTransport = new StreamableHTTPServerTransport({
35171
35539
  sessionIdGenerator: void 0,
35172
- enableJsonResponse: true
35540
+ enableJsonResponse
35173
35541
  });
35174
35542
  await transientServer.connect(tempTransport);
35175
35543
  await tempTransport.handleRequest(req, res, body);