fastmcp 1.27.6 → 2.0.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.
@@ -17,6 +17,7 @@ import { expect, test, vi } from "vitest";
17
17
  import { z } from "zod";
18
18
 
19
19
  import {
20
+ audioContent,
20
21
  type ContentResult,
21
22
  FastMCP,
22
23
  FastMCPSession,
@@ -51,11 +52,10 @@ const runWithTestServer = async ({
51
52
  });
52
53
 
53
54
  await server.start({
54
- sse: {
55
- endpoint: "/sse",
55
+ httpStream: {
56
56
  port,
57
57
  },
58
- transportType: "sse",
58
+ transportType: "httpStream",
59
59
  });
60
60
 
61
61
  try {
@@ -269,6 +269,55 @@ test("returns an image", async () => {
269
269
  });
270
270
  });
271
271
 
272
+ test("returns an audio", async () => {
273
+ await runWithTestServer({
274
+ run: async ({ client }) => {
275
+ expect(
276
+ await client.callTool({
277
+ arguments: {
278
+ a: 1,
279
+ b: 2,
280
+ },
281
+ name: "add",
282
+ }),
283
+ ).toEqual({
284
+ content: [
285
+ {
286
+ data: "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA",
287
+ mimeType: "audio/wav",
288
+ type: "audio",
289
+ },
290
+ ],
291
+ });
292
+ },
293
+ server: async () => {
294
+ const server = new FastMCP({
295
+ name: "Test",
296
+ version: "1.0.0",
297
+ });
298
+
299
+ server.addTool({
300
+ description: "Add two numbers",
301
+ execute: async () => {
302
+ return audioContent({
303
+ buffer: Buffer.from(
304
+ "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA",
305
+ "base64",
306
+ ),
307
+ });
308
+ },
309
+ name: "add",
310
+ parameters: z.object({
311
+ a: z.number(),
312
+ b: z.number(),
313
+ }),
314
+ });
315
+
316
+ return server;
317
+ },
318
+ });
319
+ });
320
+
272
321
  test("handles UserError errors", async () => {
273
322
  await runWithTestServer({
274
323
  run: async ({ client }) => {
@@ -742,11 +791,10 @@ test("uses events to notify server of client connect/disconnect", async () => {
742
791
  server.on("disconnect", onDisconnect);
743
792
 
744
793
  await server.start({
745
- sse: {
746
- endpoint: "/sse",
794
+ httpStream: {
747
795
  port,
748
796
  },
749
- transportType: "sse",
797
+ transportType: "httpStream",
750
798
  });
751
799
 
752
800
  const client = new Client(
@@ -791,11 +839,10 @@ test("handles multiple clients", async () => {
791
839
  });
792
840
 
793
841
  await server.start({
794
- sse: {
795
- endpoint: "/sse",
842
+ httpStream: {
796
843
  port,
797
844
  },
798
- transportType: "sse",
845
+ transportType: "httpStream",
799
846
  });
800
847
 
801
848
  const client1 = new Client(
@@ -1472,11 +1519,10 @@ test("allows new clients to connect after a client disconnects", async () => {
1472
1519
  });
1473
1520
 
1474
1521
  await server.start({
1475
- sse: {
1476
- endpoint: "/sse",
1522
+ httpStream: {
1477
1523
  port,
1478
1524
  },
1479
- transportType: "sse",
1525
+ transportType: "httpStream",
1480
1526
  });
1481
1527
 
1482
1528
  const client1 = new Client(
@@ -1551,11 +1597,10 @@ test("able to close server immediately after starting it", async () => {
1551
1597
  });
1552
1598
 
1553
1599
  await server.start({
1554
- sse: {
1555
- endpoint: "/sse",
1600
+ httpStream: {
1556
1601
  port,
1557
1602
  },
1558
- transportType: "sse",
1603
+ transportType: "httpStream",
1559
1604
  });
1560
1605
 
1561
1606
  // We were previously not waiting for the server to start.
@@ -1584,11 +1629,10 @@ test("closing event source does not produce error", async () => {
1584
1629
  });
1585
1630
 
1586
1631
  await server.start({
1587
- sse: {
1588
- endpoint: "/sse",
1632
+ httpStream: {
1589
1633
  port,
1590
1634
  },
1591
- transportType: "sse",
1635
+ transportType: "httpStream",
1592
1636
  });
1593
1637
 
1594
1638
  const eventSource = await new Promise<EventSourceClient>((onMessage) => {
@@ -1647,11 +1691,10 @@ test("provides auth to tools", async () => {
1647
1691
  });
1648
1692
 
1649
1693
  await server.start({
1650
- sse: {
1651
- endpoint: "/sse",
1694
+ httpStream: {
1652
1695
  port,
1653
1696
  },
1654
- transportType: "sse",
1697
+ transportType: "httpStream",
1655
1698
  });
1656
1699
 
1657
1700
  const client = new Client(
@@ -1716,10 +1759,90 @@ test("provides auth to tools", async () => {
1716
1759
  },
1717
1760
  reportProgress: expect.any(Function),
1718
1761
  session: { id: 1 },
1762
+ streamContent: expect.any(Function),
1719
1763
  },
1720
1764
  );
1721
1765
  });
1722
1766
 
1767
+ test("supports streaming output from tools", async () => {
1768
+ let streamResult: { content: Array<{ text: string; type: string }> };
1769
+
1770
+ await runWithTestServer({
1771
+ run: async ({ client }) => {
1772
+ const result = await client.callTool({
1773
+ arguments: {},
1774
+ name: "streaming-void-tool",
1775
+ });
1776
+
1777
+ expect(result).toEqual({
1778
+ content: [],
1779
+ });
1780
+
1781
+ streamResult = (await client.callTool({
1782
+ arguments: {},
1783
+ name: "streaming-with-result",
1784
+ })) as { content: Array<{ text: string; type: string }> };
1785
+
1786
+ expect(streamResult).toEqual({
1787
+ content: [{ text: "Final result after streaming", type: "text" }],
1788
+ });
1789
+ },
1790
+ server: async () => {
1791
+ const server = new FastMCP({
1792
+ name: "Test",
1793
+ version: "1.0.0",
1794
+ });
1795
+
1796
+ server.addTool({
1797
+ annotations: {
1798
+ streamingHint: true,
1799
+ },
1800
+ description: "Tool yang streaming dan mengembalikan void",
1801
+ execute: async (_args, context) => {
1802
+ await context.streamContent({
1803
+ text: "Streaming content 1",
1804
+ type: "text",
1805
+ });
1806
+
1807
+ await context.streamContent({
1808
+ text: "Streaming content 2",
1809
+ type: "text",
1810
+ });
1811
+
1812
+ // Return void
1813
+ return;
1814
+ },
1815
+ name: "streaming-void-tool",
1816
+ parameters: z.object({}),
1817
+ });
1818
+
1819
+ server.addTool({
1820
+ annotations: {
1821
+ streamingHint: true,
1822
+ },
1823
+ description: "Tool yang streaming dan mengembalikan hasil",
1824
+ execute: async (_args, context) => {
1825
+ await context.streamContent({
1826
+ text: "Streaming content 1",
1827
+ type: "text",
1828
+ });
1829
+
1830
+ await context.streamContent({
1831
+ text: "Streaming content 2",
1832
+ type: "text",
1833
+ });
1834
+
1835
+ return "Final result after streaming";
1836
+ },
1837
+ name: "streaming-with-result",
1838
+ parameters: z.object({}),
1839
+ });
1840
+
1841
+ return server;
1842
+ },
1843
+ });
1844
+ });
1845
+
1723
1846
  test("blocks unauthorized requests", async () => {
1724
1847
  const port = await getRandomPort();
1725
1848
 
@@ -1735,11 +1858,10 @@ test("blocks unauthorized requests", async () => {
1735
1858
  });
1736
1859
 
1737
1860
  await server.start({
1738
- sse: {
1739
- endpoint: "/sse",
1861
+ httpStream: {
1740
1862
  port,
1741
1863
  },
1742
- transportType: "sse",
1864
+ transportType: "httpStream",
1743
1865
  });
1744
1866
 
1745
1867
  const client = new Client(
@@ -1767,6 +1889,7 @@ test("blocks unauthorized requests", async () => {
1767
1889
  // Set longer timeout for HTTP Stream tests
1768
1890
  test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => {
1769
1891
  console.log("Starting HTTP Stream test...");
1892
+
1770
1893
  const port = await getRandomPort();
1771
1894
 
1772
1895
  // Create server directly (don't use helper function)
@@ -1789,7 +1912,6 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => {
1789
1912
 
1790
1913
  await server.start({
1791
1914
  httpStream: {
1792
- endpoint: "/httpStream",
1793
1915
  port,
1794
1916
  },
1795
1917
  transportType: "httpStream",
@@ -1810,7 +1932,7 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => {
1810
1932
  // IMPORTANT: Don't provide sessionId manually with HTTP streaming
1811
1933
  // The server will generate a session ID automatically
1812
1934
  const transport = new StreamableHTTPClientTransport(
1813
- new URL(`http://localhost:${port}/httpStream`),
1935
+ new URL(`http://localhost:${port}/stream`),
1814
1936
  );
1815
1937
 
1816
1938
  // Connect client to server
@@ -1835,6 +1957,7 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => {
1835
1957
 
1836
1958
  // Clean up connection
1837
1959
  await transport.terminateSession();
1960
+
1838
1961
  await client.close();
1839
1962
  } finally {
1840
1963
  await server.stop();
package/src/FastMCP.ts CHANGED
@@ -2,7 +2,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
4
  import {
5
- AudioContent,
6
5
  CallToolRequestSchema,
7
6
  ClientCapabilities,
8
7
  CompleteRequestSchema,
@@ -26,7 +25,7 @@ import { fileTypeFromBuffer } from "file-type";
26
25
  import { readFile } from "fs/promises";
27
26
  import Fuse from "fuse.js";
28
27
  import http from "http";
29
- import { startHTTPStreamServer, startSSEServer } from "mcp-proxy";
28
+ import { startHTTPServer } from "mcp-proxy";
30
29
  import { StrictEventEmitter } from "strict-event-emitter-types";
31
30
  import { setTimeout as delay } from "timers/promises";
32
31
  import { fetch } from "undici";
@@ -181,6 +180,7 @@ type Context<T extends FastMCPSessionAuth> = {
181
180
  };
182
181
  reportProgress: (progress: Progress) => Promise<void>;
183
182
  session: T | undefined;
183
+ streamContent: (content: Content | Content[]) => Promise<void>;
184
184
  };
185
185
 
186
186
  type Extra = unknown;
@@ -264,11 +264,29 @@ const ImageContentZodSchema = z
264
264
  })
265
265
  .strict() satisfies z.ZodType<ImageContent>;
266
266
 
267
- type Content = ImageContent | TextContent;
267
+ type AudioContent = {
268
+ data: string;
269
+ mimeType: string;
270
+ type: "audio";
271
+ };
272
+
273
+ const AudioContentZodSchema = z
274
+ .object({
275
+ /**
276
+ * The base64-encoded audio data.
277
+ */
278
+ data: z.string().base64(),
279
+ mimeType: z.string(),
280
+ type: z.literal("audio"),
281
+ })
282
+ .strict() satisfies z.ZodType<AudioContent>;
283
+
284
+ type Content = AudioContent | ImageContent | TextContent;
268
285
 
269
286
  const ContentZodSchema = z.discriminatedUnion("type", [
270
287
  TextContentZodSchema,
271
288
  ImageContentZodSchema,
289
+ AudioContentZodSchema,
272
290
  ]) satisfies z.ZodType<Content>;
273
291
 
274
292
  type ContentResult = {
@@ -466,13 +484,19 @@ type Tool<
466
484
  T extends FastMCPSessionAuth,
467
485
  Params extends ToolParameters = ToolParameters,
468
486
  > = {
469
- annotations?: ToolAnnotations;
487
+ annotations?: {
488
+ /**
489
+ * When true, the tool leverages incremental content streaming
490
+ * Return void for tools that handle all their output via streaming
491
+ */
492
+ streamingHint?: boolean;
493
+ } & ToolAnnotations;
470
494
  description?: string;
471
495
  execute: (
472
496
  args: StandardSchemaV1.InferOutput<Params>,
473
497
  context: Context<T>,
474
498
  ) => Promise<
475
- AudioContent | ContentResult | ImageContent | string | TextContent
499
+ AudioContent | ContentResult | ImageContent | string | TextContent | void
476
500
  >;
477
501
  name: string;
478
502
  parameters?: Params;
@@ -750,7 +774,7 @@ export class FastMCPSession<
750
774
 
751
775
  if ("type" in transport) {
752
776
  // Enable by default for SSE and HTTP streaming
753
- if (transport.type === "sse" || transport.type === "httpStream") {
777
+ if (transport.type === "httpStream") {
754
778
  defaultEnabled = true;
755
779
  }
756
780
  }
@@ -1249,14 +1273,29 @@ export class FastMCPSession<
1249
1273
  };
1250
1274
 
1251
1275
  // Create a promise for tool execution
1276
+ // Streams partial results while a tool is still executing
1277
+ // Enables progressive rendering and real-time feedback
1278
+ const streamContent = async (content: Content | Content[]) => {
1279
+ const contentArray = Array.isArray(content) ? content : [content];
1280
+
1281
+ await this.#server.notification({
1282
+ method: "notifications/tool/streamContent",
1283
+ params: {
1284
+ content: contentArray,
1285
+ toolName: request.params.name,
1286
+ },
1287
+ });
1288
+ };
1289
+
1252
1290
  const executeToolPromise = tool.execute(args, {
1253
1291
  log,
1254
1292
  reportProgress,
1255
1293
  session: this.#auth,
1294
+ streamContent,
1256
1295
  });
1257
1296
 
1258
1297
  // Handle timeout if specified
1259
- const maybeStringResult = await (tool.timeoutMs
1298
+ const maybeStringResult = (await (tool.timeoutMs
1260
1299
  ? Promise.race([
1261
1300
  executeToolPromise,
1262
1301
  new Promise<never>((_, reject) => {
@@ -1269,9 +1308,20 @@ export class FastMCPSession<
1269
1308
  }, tool.timeoutMs);
1270
1309
  }),
1271
1310
  ])
1272
- : executeToolPromise);
1273
-
1274
- if (typeof maybeStringResult === "string") {
1311
+ : executeToolPromise)) as
1312
+ | AudioContent
1313
+ | ContentResult
1314
+ | ImageContent
1315
+ | null
1316
+ | string
1317
+ | TextContent
1318
+ | undefined;
1319
+
1320
+ if (maybeStringResult === undefined || maybeStringResult === null) {
1321
+ result = ContentResultZodSchema.parse({
1322
+ content: [],
1323
+ });
1324
+ } else if (typeof maybeStringResult === "string") {
1275
1325
  result = ContentResultZodSchema.parse({
1276
1326
  content: [{ text: maybeStringResult, type: "text" }],
1277
1327
  });
@@ -1322,7 +1372,6 @@ export class FastMCP<
1322
1372
  #resources: Resource[] = [];
1323
1373
  #resourcesTemplates: InputResourceTemplate[] = [];
1324
1374
  #sessions: FastMCPSession<T>[] = [];
1325
- #sseServer: null | SSEServer = null;
1326
1375
 
1327
1376
  #tools: Tool<T>[] = [];
1328
1377
 
@@ -1371,13 +1420,9 @@ export class FastMCP<
1371
1420
  public async start(
1372
1421
  options:
1373
1422
  | {
1374
- httpStream: { endpoint: `/${string}`; port: number };
1423
+ httpStream: { port: number };
1375
1424
  transportType: "httpStream";
1376
1425
  }
1377
- | {
1378
- sse: { endpoint: `/${string}`; port: number };
1379
- transportType: "sse";
1380
- }
1381
1426
  | { transportType: "stdio" } = {
1382
1427
  transportType: "stdio",
1383
1428
  },
@@ -1404,48 +1449,8 @@ export class FastMCP<
1404
1449
  this.emit("connect", {
1405
1450
  session,
1406
1451
  });
1407
- } else if (options.transportType === "sse") {
1408
- this.#sseServer = await startSSEServer<FastMCPSession<T>>({
1409
- createServer: async (request) => {
1410
- let auth: T | undefined;
1411
-
1412
- if (this.#authenticate) {
1413
- auth = await this.#authenticate(request);
1414
- }
1415
-
1416
- return new FastMCPSession<T>({
1417
- auth,
1418
- name: this.#options.name,
1419
- ping: this.#options.ping,
1420
- prompts: this.#prompts,
1421
- resources: this.#resources,
1422
- resourcesTemplates: this.#resourcesTemplates,
1423
- roots: this.#options.roots,
1424
- tools: this.#tools,
1425
- version: this.#options.version,
1426
- });
1427
- },
1428
- endpoint: options.sse.endpoint as `/${string}`,
1429
- onClose: (session) => {
1430
- this.emit("disconnect", {
1431
- session,
1432
- });
1433
- },
1434
- onConnect: async (session) => {
1435
- this.#sessions.push(session);
1436
-
1437
- this.emit("connect", {
1438
- session,
1439
- });
1440
- },
1441
- port: options.sse.port,
1442
- });
1443
-
1444
- console.info(
1445
- `[FastMCP info] server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,
1446
- );
1447
1452
  } else if (options.transportType === "httpStream") {
1448
- this.#httpStreamServer = await startHTTPStreamServer<FastMCPSession<T>>({
1453
+ this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
1449
1454
  createServer: async (request) => {
1450
1455
  let auth: T | undefined;
1451
1456
 
@@ -1465,7 +1470,6 @@ export class FastMCP<
1465
1470
  version: this.#options.version,
1466
1471
  });
1467
1472
  },
1468
- endpoint: options.httpStream.endpoint as `/${string}`,
1469
1473
  onClose: (session) => {
1470
1474
  this.emit("disconnect", {
1471
1475
  session,
@@ -1482,7 +1486,7 @@ export class FastMCP<
1482
1486
  });
1483
1487
 
1484
1488
  console.info(
1485
- `[FastMCP info] server is running on HTTP Stream at http://localhost:${options.httpStream.port}${options.httpStream.endpoint}`,
1489
+ `[FastMCP info] server is running on HTTP Stream at http://localhost:${options.httpStream.port}/stream`,
1486
1490
  );
1487
1491
  } else {
1488
1492
  throw new Error("Invalid transport type");
@@ -1493,9 +1497,6 @@ export class FastMCP<
1493
1497
  * Stops the server.
1494
1498
  */
1495
1499
  public async stop() {
1496
- if (this.#sseServer) {
1497
- await this.#sseServer.close();
1498
- }
1499
1500
  if (this.#httpStreamServer) {
1500
1501
  await this.#httpStreamServer.close();
1501
1502
  }
@@ -1,8 +1,12 @@
1
1
  /**
2
- * This is an example of a FastMCP server that adds two numbers.
2
+ * Example FastMCP server demonstrating core functionality plus streaming output.
3
3
  *
4
- * If you are looking for a complete example of an MCP server repository,
5
- * see https://github.com/punkpeye/fastmcp-boilerplate
4
+ * Features demonstrated:
5
+ * - Basic tool with type-safe parameters
6
+ * - Streaming-enabled tool for incremental output
7
+ * - Advanced tool annotations
8
+ *
9
+ * For a complete project template, see https://github.com/punkpeye/fastmcp-boilerplate
6
10
  */
7
11
  import { type } from "arktype";
8
12
  import * as v from "valibot";
@@ -118,6 +122,39 @@ server.addResource({
118
122
  uri: "file:///logs/app.log",
119
123
  });
120
124
 
125
+ server.addTool({
126
+ annotations: {
127
+ openWorldHint: false,
128
+ readOnlyHint: true,
129
+ streamingHint: true,
130
+ },
131
+ description: "Generate a poem line by line with streaming output",
132
+ execute: async (args, context) => {
133
+ const { theme } = args;
134
+ const lines = [
135
+ `Poem about ${theme} - line 1`,
136
+ `Poem about ${theme} - line 2`,
137
+ `Poem about ${theme} - line 3`,
138
+ `Poem about ${theme} - line 4`,
139
+ ];
140
+
141
+ for (const line of lines) {
142
+ await context.streamContent({
143
+ text: line,
144
+ type: "text",
145
+ });
146
+
147
+ await new Promise((resolve) => setTimeout(resolve, 1000));
148
+ }
149
+
150
+ return;
151
+ },
152
+ name: "stream-poem",
153
+ parameters: z.object({
154
+ theme: z.string().describe("Theme for the poem"),
155
+ }),
156
+ });
157
+
121
158
  server.addPrompt({
122
159
  arguments: [
123
160
  {
@@ -144,7 +181,6 @@ if (transportType === "httpStream") {
144
181
 
145
182
  server.start({
146
183
  httpStream: {
147
- endpoint: "/stream",
148
184
  port: PORT,
149
185
  },
150
186
  transportType: "httpStream",