fastmcp 4.10.0 → 4.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 CHANGED
@@ -33,6 +33,7 @@ A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable
33
33
  - [Configurable ping behavior](#configurable-ping-behavior)
34
34
  - [Health-check endpoint](#health-check-endpoint)
35
35
  - [Roots](#roots-management)
36
+ - [In-memory transport](#unit-testing-with-an-in-memory-transport) for unit testing without binding a port
36
37
  - CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector)
37
38
 
38
39
  ## When to use FastMCP over the official SDK?
@@ -1228,15 +1229,32 @@ server.addTool({
1228
1229
  });
1229
1230
  ```
1230
1231
 
1232
+ `reportProgress` accepts an optional human-readable `message` alongside the numeric fields, which clients can display next to the progress indicator:
1233
+
1234
+ ```js
1235
+ await reportProgress({
1236
+ progress: 40,
1237
+ total: 100,
1238
+ message: "Downloading chunk 4 of 10…",
1239
+ });
1240
+ ```
1241
+
1242
+ Progress notifications are only emitted when the client opts in by supplying a `progressToken` on the tool call; otherwise `reportProgress` is a no-op. Because `notifications/progress` is part of the MCP specification (the `message` field since revision 2025-03-26), this is the portable way to send incremental updates during a long-running tool call — see [Streaming Output](#streaming-output) below for the difference.
1243
+
1231
1244
  #### Streaming Output
1232
1245
 
1233
- FastMCP supports streaming partial results from tools while they're still executing, enabling responsive UIs and real-time feedback. This is particularly useful for:
1246
+ FastMCP can stream partial results from tools while they're still executing, enabling responsive UIs and real-time feedback. This is particularly useful for:
1234
1247
 
1235
1248
  - Long-running operations that generate content incrementally
1236
1249
  - Progressive generation of text, images, or other media
1237
1250
  - Operations where users benefit from seeing immediate partial results
1238
1251
 
1239
- To enable streaming for a tool, add the `streamingHint` annotation and use the `streamContent` method:
1252
+ > [!IMPORTANT]
1253
+ > `streamContent` is a **FastMCP extension, not part of the MCP specification**. It emits a `notifications/tool/streamContent` notification, which the MCP specification does not define — as of revision `2025-11-25` there is no standard mechanism for streaming tool output ([SEP-2998](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2998) is the in-progress proposal to add one).
1254
+ >
1255
+ > Clients discard notifications they have no handler registered for, silently and without error. A client only sees streamed content if it registers a handler for the method (or sets a `fallbackNotificationHandler`), and **no client is known to render it as tool output** — MCP Inspector, for example, logs it in its notifications pane via a fallback handler, but the tool result itself still shows only what `execute` returned. Streaming is therefore mainly useful when you also control the client — see [Consuming streamed content](#consuming-streamed-content) below. If you need incremental updates that work on any client, use [`reportProgress`](#progress) with a `message` instead.
1256
+
1257
+ To stream from a tool, use the `streamContent` method:
1240
1258
 
1241
1259
  ```js
1242
1260
  server.addTool({
@@ -1246,7 +1264,7 @@ server.addTool({
1246
1264
  prompt: z.string(),
1247
1265
  }),
1248
1266
  annotations: {
1249
- streamingHint: true, // Signals this tool uses streaming
1267
+ streamingHint: true, // Advisory only; see below
1250
1268
  readOnlyHint: true,
1251
1269
  },
1252
1270
  execute: async (args, { streamContent }) => {
@@ -1260,19 +1278,45 @@ server.addTool({
1260
1278
  await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay
1261
1279
  }
1262
1280
 
1263
- // When using streamContent, you can:
1264
- // 1. Return void (if all content was streamed)
1265
- // 2. Return a final result (which will be appended to streamed content)
1281
+ // Always return a final result. Returning nothing sends an empty tool
1282
+ // result, so clients that ignore the streamed notifications see no output
1283
+ // at all.
1284
+ return "The quick brown fox jumps over the lazy dog.";
1285
+ },
1286
+ });
1287
+ ```
1266
1288
 
1267
- // Option 1: All content was streamed, so return void
1268
- return;
1289
+ > [!WARNING]
1290
+ > Returning `undefined` from `execute` produces a tool result with empty `content`. If you stream everything and return nothing, the tool call resolves to an empty result with no indication that anything was lost — including on clients that do log the notification. Return the complete result as well, and treat streamed content purely as a progressive-rendering enhancement.
1269
1291
 
1270
- // Option 2: Return final content that will be appended
1271
- // return "Generation complete!";
1272
- },
1292
+ The `streamingHint` annotation is advisory metadata. It is forwarded verbatim to clients in `tools/list`, but it does not enable or gate `streamContent`, and FastMCP itself never reads it. No client is known to act on it today, though [SEP-2998](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2998) proposes standardizing the same annotation name.
1293
+
1294
+ ##### Consuming streamed content
1295
+
1296
+ A client sees these notifications only if it registers a handler for the method (or sets a `fallbackNotificationHandler`):
1297
+
1298
+ ```ts
1299
+ import { z } from "zod";
1300
+
1301
+ const StreamContentNotificationSchema = z.object({
1302
+ method: z.literal("notifications/tool/streamContent"),
1303
+ params: z.object({
1304
+ content: z.array(z.any()),
1305
+ toolName: z.string(),
1306
+ }),
1273
1307
  });
1308
+
1309
+ client.setNotificationHandler(
1310
+ StreamContentNotificationSchema,
1311
+ (notification) => {
1312
+ const { content, toolName } = notification.params;
1313
+ // Render the partial content however you like.
1314
+ },
1315
+ );
1274
1316
  ```
1275
1317
 
1318
+ Note that notifications carry only `toolName`, not a request or progress token, so concurrent calls to the same tool on one session cannot be told apart.
1319
+
1276
1320
  Streaming works with all content types (text, image, audio) and can be combined with progress reporting:
1277
1321
 
1278
1322
  ```js
@@ -1289,10 +1333,14 @@ server.addTool({
1289
1333
  const total = args.datasetSize;
1290
1334
 
1291
1335
  for (let i = 0; i < total; i++) {
1292
- // Report numeric progress
1293
- await reportProgress({ progress: i, total });
1336
+ // Standard progress notification: reaches every spec-compliant client
1337
+ await reportProgress({
1338
+ progress: i,
1339
+ total,
1340
+ message: `Processed ${i} of ${total} items`,
1341
+ });
1294
1342
 
1295
- // Stream intermediate results
1343
+ // Richer partial content: only reaches clients that opt in
1296
1344
  if (i % 10 === 0) {
1297
1345
  await streamContent({
1298
1346
  type: "text",
@@ -2478,6 +2526,51 @@ session.on("error", (event) => {
2478
2526
 
2479
2527
  ## Running Your Server
2480
2528
 
2529
+ ### Unit testing with an in-memory transport
2530
+
2531
+ `server.connect(transport)` attaches the server to a transport you construct yourself, instead of letting `start()` create one. Paired with the SDK's `InMemoryTransport`, this lets you drive a server in-process — no port to bind, no subprocess to spawn — which is usually what you want for testing a `stdio` server:
2532
+
2533
+ ```ts
2534
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2535
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
2536
+
2537
+ async function createTestClient(server: FastMCP) {
2538
+ const [clientTransport, serverTransport] =
2539
+ InMemoryTransport.createLinkedPair();
2540
+
2541
+ const client = new Client({ name: "test-client", version: "0.0.0" });
2542
+
2543
+ const [session] = await Promise.all([
2544
+ server.connect(serverTransport),
2545
+ client.connect(clientTransport),
2546
+ ]);
2547
+
2548
+ return { client, session };
2549
+ }
2550
+
2551
+ test("adds two numbers", async () => {
2552
+ const { client } = await createTestClient(server);
2553
+
2554
+ expect(
2555
+ await client.callTool({ arguments: { a: 2, b: 3 }, name: "add" }),
2556
+ ).toEqual({
2557
+ content: [{ text: "5", type: "text" }],
2558
+ });
2559
+
2560
+ await client.close();
2561
+ });
2562
+ ```
2563
+
2564
+ The session is built from the tools, resources and prompts registered on the instance, exactly as `start()` builds it, so your tests exercise the same wiring the real server uses — including `canAccess` filtering and the `connect`/`disconnect` events.
2565
+
2566
+ Pass session auth as the second argument, equivalent to what your `authenticate` function would return:
2567
+
2568
+ ```ts
2569
+ await server.connect(serverTransport, { id: 7, role: "admin" });
2570
+ ```
2571
+
2572
+ `connect` returns the [`FastMCPSession`](#fastmcpsession), so you can assert on `session.clientCapabilities`, `session.roots`, and the rest. The transport's lifecycle belongs to you: `stop()` does not close transports passed to `connect`, so close the client (and the session, if you need `disconnect` to fire) when the test finishes.
2573
+
2481
2574
  ### Test with `mcp-cli`
2482
2575
 
2483
2576
  The fastest way to test and debug your server is with `fastmcp dev`:
package/dist/FastMCP.cjs CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
 
10
10
 
11
- var _chunkGF4ORBWZcjs = require('./chunk-GF4ORBWZ.cjs');
11
+ var _chunkPGTM33AUcjs = require('./chunk-PGTM33AU.cjs');
12
12
 
13
13
 
14
14
 
@@ -43,5 +43,5 @@ var _chunkDZYT6QAHcjs = require('./chunk-DZYT6QAH.cjs');
43
43
 
44
44
 
45
45
 
46
- exports.AuthProvider = _chunkDZYT6QAHcjs.AuthProvider; exports.AzureProvider = _chunkDZYT6QAHcjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkGF4ORBWZcjs.DiscoveryDocumentCache; exports.FastMCP = _chunkGF4ORBWZcjs.FastMCP; exports.FastMCPSession = _chunkGF4ORBWZcjs.FastMCPSession; exports.GitHubProvider = _chunkDZYT6QAHcjs.GitHubProvider; exports.GoogleProvider = _chunkDZYT6QAHcjs.GoogleProvider; exports.OAuthProvider = _chunkDZYT6QAHcjs.OAuthProvider; exports.ServerState = _chunkGF4ORBWZcjs.ServerState; exports.UnexpectedStateError = _chunkGF4ORBWZcjs.UnexpectedStateError; exports.UserError = _chunkGF4ORBWZcjs.UserError; exports.audioContent = _chunkGF4ORBWZcjs.audioContent; exports.getAuthSession = _chunkDZYT6QAHcjs.getAuthSession; exports.imageContent = _chunkGF4ORBWZcjs.imageContent; exports.jsonSchemaAdapter = _chunkGF4ORBWZcjs.jsonSchemaAdapter; exports.requireAll = _chunkDZYT6QAHcjs.requireAll; exports.requireAny = _chunkDZYT6QAHcjs.requireAny; exports.requireAuth = _chunkDZYT6QAHcjs.requireAuth; exports.requireRole = _chunkDZYT6QAHcjs.requireRole; exports.requireScopes = _chunkDZYT6QAHcjs.requireScopes;
46
+ exports.AuthProvider = _chunkDZYT6QAHcjs.AuthProvider; exports.AzureProvider = _chunkDZYT6QAHcjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkPGTM33AUcjs.DiscoveryDocumentCache; exports.FastMCP = _chunkPGTM33AUcjs.FastMCP; exports.FastMCPSession = _chunkPGTM33AUcjs.FastMCPSession; exports.GitHubProvider = _chunkDZYT6QAHcjs.GitHubProvider; exports.GoogleProvider = _chunkDZYT6QAHcjs.GoogleProvider; exports.OAuthProvider = _chunkDZYT6QAHcjs.OAuthProvider; exports.ServerState = _chunkPGTM33AUcjs.ServerState; exports.UnexpectedStateError = _chunkPGTM33AUcjs.UnexpectedStateError; exports.UserError = _chunkPGTM33AUcjs.UserError; exports.audioContent = _chunkPGTM33AUcjs.audioContent; exports.getAuthSession = _chunkDZYT6QAHcjs.getAuthSession; exports.imageContent = _chunkPGTM33AUcjs.imageContent; exports.jsonSchemaAdapter = _chunkPGTM33AUcjs.jsonSchemaAdapter; exports.requireAll = _chunkDZYT6QAHcjs.requireAll; exports.requireAny = _chunkDZYT6QAHcjs.requireAny; exports.requireAuth = _chunkDZYT6QAHcjs.requireAuth; exports.requireRole = _chunkDZYT6QAHcjs.requireRole; exports.requireScopes = _chunkDZYT6QAHcjs.requireScopes;
47
47
  //# sourceMappingURL=FastMCP.cjs.map
@@ -185,6 +185,22 @@ type Context<T extends FastMCPSessionAuth> = {
185
185
  * counters, or maintain user-specific data across multiple requests.
186
186
  */
187
187
  sessionId?: string;
188
+ /**
189
+ * Streams incremental content while the tool is still executing, by emitting
190
+ * a `notifications/tool/streamContent` notification.
191
+ *
192
+ * NOTE: this is a FastMCP extension, not part of the MCP specification. As of
193
+ * revision 2025-11-25 the spec has no streaming tool output primitive (see
194
+ * SEP-2998 for the in-progress proposal). A client only receives these
195
+ * notifications if it registers a handler for the method or sets a
196
+ * `fallbackNotificationHandler`; otherwise the SDK drops them silently. No
197
+ * client is known to render them as tool output.
198
+ *
199
+ * Always return a final result from `execute` rather than relying on streamed
200
+ * content alone, otherwise clients that ignore the notification see an empty
201
+ * tool result. For incremental status that works everywhere, prefer
202
+ * {@link Context.reportProgress} with a `message`.
203
+ */
188
204
  streamContent: (content: Content | Content[]) => Promise<void>;
189
205
  };
190
206
  type Extra = unknown;
@@ -199,6 +215,13 @@ type Literal = boolean | null | number | string | undefined;
199
215
  */
200
216
  type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "streamContent">;
201
217
  type Progress = {
218
+ /**
219
+ * An optional human-readable message describing the current progress.
220
+ *
221
+ * Part of `notifications/progress` since MCP revision 2025-03-26, so unlike
222
+ * `streamContent` this reaches any spec-compliant client.
223
+ */
224
+ message?: string;
202
225
  /**
203
226
  * The progress thus far. This should increase every time progress is made, even if the total is unknown.
204
227
  */
@@ -681,8 +704,11 @@ type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolPara
681
704
  };
682
705
  annotations?: {
683
706
  /**
684
- * When true, the tool leverages incremental content streaming
685
- * Return void for tools that handle all their output via streaming
707
+ * Advisory metadata signalling that the tool streams incremental content
708
+ * via {@link Context.streamContent}. Forwarded verbatim in `tools/list`.
709
+ *
710
+ * This has no effect on FastMCP's behavior: it neither enables nor is
711
+ * required by `streamContent`. No known client interprets it today.
686
712
  */
687
713
  streamingHint?: boolean;
688
714
  } & ToolAnnotations;
@@ -888,6 +914,34 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
888
914
  * Adds tools to the server.
889
915
  */
890
916
  addTools<Params extends ToolParameters>(tools: Tool<T, Params>[]): void;
917
+ /**
918
+ * Connects the server to a transport you constructed yourself, instead of
919
+ * letting {@link FastMCP.start} create one.
920
+ *
921
+ * The session is built from the tools, resources and prompts registered on
922
+ * this instance — exactly as `start()` does — so tests exercise the same
923
+ * wiring the real server uses. The main use case is driving a server
924
+ * in-process over `InMemoryTransport` without binding a port:
925
+ *
926
+ * ```ts
927
+ * const [clientTransport, serverTransport] =
928
+ * InMemoryTransport.createLinkedPair();
929
+ *
930
+ * await Promise.all([
931
+ * server.connect(serverTransport),
932
+ * client.connect(clientTransport),
933
+ * ]);
934
+ * ```
935
+ *
936
+ * The transport's lifecycle belongs to the caller: `stop()` does not close
937
+ * transports passed here. Close the returned session (or the transport) when
938
+ * you are done with it.
939
+ *
940
+ * @param transport - An already-constructed MCP server transport.
941
+ * @param auth - Session auth, equivalent to what `authenticate` would return.
942
+ * @returns The session bound to the transport.
943
+ */
944
+ connect(transport: Transport, auth?: T): Promise<FastMCPSession<T>>;
891
945
  /**
892
946
  * Embeds a resource by URI, making it easy to include resources in tool responses.
893
947
  *
package/dist/FastMCP.d.ts CHANGED
@@ -185,6 +185,22 @@ type Context<T extends FastMCPSessionAuth> = {
185
185
  * counters, or maintain user-specific data across multiple requests.
186
186
  */
187
187
  sessionId?: string;
188
+ /**
189
+ * Streams incremental content while the tool is still executing, by emitting
190
+ * a `notifications/tool/streamContent` notification.
191
+ *
192
+ * NOTE: this is a FastMCP extension, not part of the MCP specification. As of
193
+ * revision 2025-11-25 the spec has no streaming tool output primitive (see
194
+ * SEP-2998 for the in-progress proposal). A client only receives these
195
+ * notifications if it registers a handler for the method or sets a
196
+ * `fallbackNotificationHandler`; otherwise the SDK drops them silently. No
197
+ * client is known to render them as tool output.
198
+ *
199
+ * Always return a final result from `execute` rather than relying on streamed
200
+ * content alone, otherwise clients that ignore the notification see an empty
201
+ * tool result. For incremental status that works everywhere, prefer
202
+ * {@link Context.reportProgress} with a `message`.
203
+ */
188
204
  streamContent: (content: Content | Content[]) => Promise<void>;
189
205
  };
190
206
  type Extra = unknown;
@@ -199,6 +215,13 @@ type Literal = boolean | null | number | string | undefined;
199
215
  */
200
216
  type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "streamContent">;
201
217
  type Progress = {
218
+ /**
219
+ * An optional human-readable message describing the current progress.
220
+ *
221
+ * Part of `notifications/progress` since MCP revision 2025-03-26, so unlike
222
+ * `streamContent` this reaches any spec-compliant client.
223
+ */
224
+ message?: string;
202
225
  /**
203
226
  * The progress thus far. This should increase every time progress is made, even if the total is unknown.
204
227
  */
@@ -681,8 +704,11 @@ type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolPara
681
704
  };
682
705
  annotations?: {
683
706
  /**
684
- * When true, the tool leverages incremental content streaming
685
- * Return void for tools that handle all their output via streaming
707
+ * Advisory metadata signalling that the tool streams incremental content
708
+ * via {@link Context.streamContent}. Forwarded verbatim in `tools/list`.
709
+ *
710
+ * This has no effect on FastMCP's behavior: it neither enables nor is
711
+ * required by `streamContent`. No known client interprets it today.
686
712
  */
687
713
  streamingHint?: boolean;
688
714
  } & ToolAnnotations;
@@ -888,6 +914,34 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
888
914
  * Adds tools to the server.
889
915
  */
890
916
  addTools<Params extends ToolParameters>(tools: Tool<T, Params>[]): void;
917
+ /**
918
+ * Connects the server to a transport you constructed yourself, instead of
919
+ * letting {@link FastMCP.start} create one.
920
+ *
921
+ * The session is built from the tools, resources and prompts registered on
922
+ * this instance — exactly as `start()` does — so tests exercise the same
923
+ * wiring the real server uses. The main use case is driving a server
924
+ * in-process over `InMemoryTransport` without binding a port:
925
+ *
926
+ * ```ts
927
+ * const [clientTransport, serverTransport] =
928
+ * InMemoryTransport.createLinkedPair();
929
+ *
930
+ * await Promise.all([
931
+ * server.connect(serverTransport),
932
+ * client.connect(clientTransport),
933
+ * ]);
934
+ * ```
935
+ *
936
+ * The transport's lifecycle belongs to the caller: `stop()` does not close
937
+ * transports passed here. Close the returned session (or the transport) when
938
+ * you are done with it.
939
+ *
940
+ * @param transport - An already-constructed MCP server transport.
941
+ * @param auth - Session auth, equivalent to what `authenticate` would return.
942
+ * @returns The session bound to the transport.
943
+ */
944
+ connect(transport: Transport, auth?: T): Promise<FastMCPSession<T>>;
891
945
  /**
892
946
  * Embeds a resource by URI, making it easy to include resources in tool responses.
893
947
  *
package/dist/FastMCP.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  audioContent,
9
9
  imageContent,
10
10
  jsonSchemaAdapter
11
- } from "./chunk-MGP2FVUH.js";
11
+ } from "./chunk-WPAZXREN.js";
12
12
  import {
13
13
  AuthProvider,
14
14
  AzureProvider,
@@ -1559,6 +1559,53 @@ var FastMCP = class extends FastMCPEventEmitter {
1559
1559
  this.#toolsListChanged(this.#tools);
1560
1560
  }
1561
1561
  }
1562
+ /**
1563
+ * Connects the server to a transport you constructed yourself, instead of
1564
+ * letting {@link FastMCP.start} create one.
1565
+ *
1566
+ * The session is built from the tools, resources and prompts registered on
1567
+ * this instance — exactly as `start()` does — so tests exercise the same
1568
+ * wiring the real server uses. The main use case is driving a server
1569
+ * in-process over `InMemoryTransport` without binding a port:
1570
+ *
1571
+ * ```ts
1572
+ * const [clientTransport, serverTransport] =
1573
+ * InMemoryTransport.createLinkedPair();
1574
+ *
1575
+ * await Promise.all([
1576
+ * server.connect(serverTransport),
1577
+ * client.connect(clientTransport),
1578
+ * ]);
1579
+ * ```
1580
+ *
1581
+ * The transport's lifecycle belongs to the caller: `stop()` does not close
1582
+ * transports passed here. Close the returned session (or the transport) when
1583
+ * you are done with it.
1584
+ *
1585
+ * @param transport - An already-constructed MCP server transport.
1586
+ * @param auth - Session auth, equivalent to what `authenticate` would return.
1587
+ * @returns The session bound to the transport.
1588
+ */
1589
+ async connect(transport, auth) {
1590
+ const session = this.#createSession(auth);
1591
+ await session.connect(transport);
1592
+ this.#sessions.push(session);
1593
+ session.once("error", () => {
1594
+ this.#removeSession(session);
1595
+ });
1596
+ const originalOnClose = transport.onclose;
1597
+ transport.onclose = () => {
1598
+ this.#removeSession(session);
1599
+ if (originalOnClose) {
1600
+ originalOnClose();
1601
+ }
1602
+ };
1603
+ this.emit("connect", {
1604
+ session
1605
+ });
1606
+ this.#serverState = "running" /* Running */;
1607
+ return session;
1608
+ }
1562
1609
  /**
1563
1610
  * Embeds a resource by URI, making it easy to include resources in tool responses.
1564
1611
  *
@@ -2445,4 +2492,4 @@ var FastMCP = class extends FastMCPEventEmitter {
2445
2492
 
2446
2493
 
2447
2494
  exports.DiscoveryDocumentCache = DiscoveryDocumentCache; exports.jsonSchemaAdapter = jsonSchemaAdapter; exports.imageContent = imageContent; exports.audioContent = audioContent; exports.UnexpectedStateError = UnexpectedStateError; exports.UserError = UserError; exports.ServerState = ServerState; exports.FastMCPSession = FastMCPSession; exports.FastMCP = FastMCP;
2448
- //# sourceMappingURL=chunk-GF4ORBWZ.cjs.map
2495
+ //# sourceMappingURL=chunk-PGTM33AU.cjs.map