This is page 48 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /context/fastmcp-docs.txt: -------------------------------------------------------------------------------- ``` 1 | Directory Structure: 2 | 3 | └── ./ 4 | ├── src 5 | │ ├── bin 6 | │ │ └── fastmcp.ts 7 | │ ├── examples 8 | │ │ └── addition.ts 9 | │ ├── FastMCP.test.ts 10 | │ └── FastMCP.ts 11 | ├── eslint.config.js 12 | ├── package.json 13 | ├── README.md 14 | └── vitest.config.js 15 | 16 | 17 | 18 | --- 19 | File: /src/bin/fastmcp.ts 20 | --- 21 | 22 | #!/usr/bin/env node 23 | 24 | import yargs from "yargs"; 25 | import { hideBin } from "yargs/helpers"; 26 | import { execa } from "execa"; 27 | 28 | await yargs(hideBin(process.argv)) 29 | .scriptName("fastmcp") 30 | .command( 31 | "dev <file>", 32 | "Start a development server", 33 | (yargs) => { 34 | return yargs.positional("file", { 35 | type: "string", 36 | describe: "The path to the server file", 37 | demandOption: true, 38 | }); 39 | }, 40 | async (argv) => { 41 | try { 42 | await execa({ 43 | stdin: "inherit", 44 | stdout: "inherit", 45 | stderr: "inherit", 46 | })`npx @wong2/mcp-cli npx tsx ${argv.file}`; 47 | } catch { 48 | process.exit(1); 49 | } 50 | }, 51 | ) 52 | .command( 53 | "inspect <file>", 54 | "Inspect a server file", 55 | (yargs) => { 56 | return yargs.positional("file", { 57 | type: "string", 58 | describe: "The path to the server file", 59 | demandOption: true, 60 | }); 61 | }, 62 | async (argv) => { 63 | try { 64 | await execa({ 65 | stdout: "inherit", 66 | stderr: "inherit", 67 | })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; 68 | } catch { 69 | process.exit(1); 70 | } 71 | }, 72 | ) 73 | .help() 74 | .parseAsync(); 75 | 76 | 77 | 78 | --- 79 | File: /src/examples/addition.ts 80 | --- 81 | 82 | /** 83 | * This is a complete example of an MCP server. 84 | */ 85 | import { FastMCP } from "../FastMCP.js"; 86 | import { z } from "zod"; 87 | 88 | const server = new FastMCP({ 89 | name: "Addition", 90 | version: "1.0.0", 91 | }); 92 | 93 | server.addTool({ 94 | name: "add", 95 | description: "Add two numbers", 96 | parameters: z.object({ 97 | a: z.number(), 98 | b: z.number(), 99 | }), 100 | execute: async (args) => { 101 | return String(args.a + args.b); 102 | }, 103 | }); 104 | 105 | server.addResource({ 106 | uri: "file:///logs/app.log", 107 | name: "Application Logs", 108 | mimeType: "text/plain", 109 | async load() { 110 | return { 111 | text: "Example log content", 112 | }; 113 | }, 114 | }); 115 | 116 | server.addPrompt({ 117 | name: "git-commit", 118 | description: "Generate a Git commit message", 119 | arguments: [ 120 | { 121 | name: "changes", 122 | description: "Git diff or description of changes", 123 | required: true, 124 | }, 125 | ], 126 | load: async (args) => { 127 | return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; 128 | }, 129 | }); 130 | 131 | server.start({ 132 | transportType: "stdio", 133 | }); 134 | 135 | 136 | 137 | --- 138 | File: /src/FastMCP.test.ts 139 | --- 140 | 141 | import { FastMCP, FastMCPSession, UserError, imageContent } from "./FastMCP.js"; 142 | import { z } from "zod"; 143 | import { test, expect, vi } from "vitest"; 144 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 145 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; 146 | import { getRandomPort } from "get-port-please"; 147 | import { setTimeout as delay } from "timers/promises"; 148 | import { 149 | CreateMessageRequestSchema, 150 | ErrorCode, 151 | ListRootsRequestSchema, 152 | LoggingMessageNotificationSchema, 153 | McpError, 154 | PingRequestSchema, 155 | Root, 156 | } from "@modelcontextprotocol/sdk/types.js"; 157 | import { createEventSource, EventSourceClient } from 'eventsource-client'; 158 | 159 | const runWithTestServer = async ({ 160 | run, 161 | client: createClient, 162 | server: createServer, 163 | }: { 164 | server?: () => Promise<FastMCP>; 165 | client?: () => Promise<Client>; 166 | run: ({ 167 | client, 168 | server, 169 | }: { 170 | client: Client; 171 | server: FastMCP; 172 | session: FastMCPSession; 173 | }) => Promise<void>; 174 | }) => { 175 | const port = await getRandomPort(); 176 | 177 | const server = createServer 178 | ? await createServer() 179 | : new FastMCP({ 180 | name: "Test", 181 | version: "1.0.0", 182 | }); 183 | 184 | await server.start({ 185 | transportType: "sse", 186 | sse: { 187 | endpoint: "/sse", 188 | port, 189 | }, 190 | }); 191 | 192 | try { 193 | const client = createClient 194 | ? await createClient() 195 | : new Client( 196 | { 197 | name: "example-client", 198 | version: "1.0.0", 199 | }, 200 | { 201 | capabilities: {}, 202 | }, 203 | ); 204 | 205 | const transport = new SSEClientTransport( 206 | new URL(`http://localhost:${port}/sse`), 207 | ); 208 | 209 | const session = await new Promise<FastMCPSession>((resolve) => { 210 | server.on("connect", (event) => { 211 | 212 | resolve(event.session); 213 | }); 214 | 215 | client.connect(transport); 216 | }); 217 | 218 | await run({ client, server, session }); 219 | } finally { 220 | await server.stop(); 221 | } 222 | 223 | return port; 224 | }; 225 | 226 | test("adds tools", async () => { 227 | await runWithTestServer({ 228 | server: async () => { 229 | const server = new FastMCP({ 230 | name: "Test", 231 | version: "1.0.0", 232 | }); 233 | 234 | server.addTool({ 235 | name: "add", 236 | description: "Add two numbers", 237 | parameters: z.object({ 238 | a: z.number(), 239 | b: z.number(), 240 | }), 241 | execute: async (args) => { 242 | return String(args.a + args.b); 243 | }, 244 | }); 245 | 246 | return server; 247 | }, 248 | run: async ({ client }) => { 249 | expect(await client.listTools()).toEqual({ 250 | tools: [ 251 | { 252 | name: "add", 253 | description: "Add two numbers", 254 | inputSchema: { 255 | additionalProperties: false, 256 | $schema: "http://json-schema.org/draft-07/schema#", 257 | type: "object", 258 | properties: { 259 | a: { type: "number" }, 260 | b: { type: "number" }, 261 | }, 262 | required: ["a", "b"], 263 | }, 264 | }, 265 | ], 266 | }); 267 | }, 268 | }); 269 | }); 270 | 271 | test("calls a tool", async () => { 272 | await runWithTestServer({ 273 | server: async () => { 274 | const server = new FastMCP({ 275 | name: "Test", 276 | version: "1.0.0", 277 | }); 278 | 279 | server.addTool({ 280 | name: "add", 281 | description: "Add two numbers", 282 | parameters: z.object({ 283 | a: z.number(), 284 | b: z.number(), 285 | }), 286 | execute: async (args) => { 287 | return String(args.a + args.b); 288 | }, 289 | }); 290 | 291 | return server; 292 | }, 293 | run: async ({ client }) => { 294 | expect( 295 | await client.callTool({ 296 | name: "add", 297 | arguments: { 298 | a: 1, 299 | b: 2, 300 | }, 301 | }), 302 | ).toEqual({ 303 | content: [{ type: "text", text: "3" }], 304 | }); 305 | }, 306 | }); 307 | }); 308 | 309 | test("returns a list", async () => { 310 | await runWithTestServer({ 311 | server: async () => { 312 | const server = new FastMCP({ 313 | name: "Test", 314 | version: "1.0.0", 315 | }); 316 | 317 | server.addTool({ 318 | name: "add", 319 | description: "Add two numbers", 320 | parameters: z.object({ 321 | a: z.number(), 322 | b: z.number(), 323 | }), 324 | execute: async () => { 325 | return { 326 | content: [ 327 | { type: "text", text: "a" }, 328 | { type: "text", text: "b" }, 329 | ], 330 | }; 331 | }, 332 | }); 333 | 334 | return server; 335 | }, 336 | run: async ({ client }) => { 337 | expect( 338 | await client.callTool({ 339 | name: "add", 340 | arguments: { 341 | a: 1, 342 | b: 2, 343 | }, 344 | }), 345 | ).toEqual({ 346 | content: [ 347 | { type: "text", text: "a" }, 348 | { type: "text", text: "b" }, 349 | ], 350 | }); 351 | }, 352 | }); 353 | }); 354 | 355 | test("returns an image", async () => { 356 | await runWithTestServer({ 357 | server: async () => { 358 | const server = new FastMCP({ 359 | name: "Test", 360 | version: "1.0.0", 361 | }); 362 | 363 | server.addTool({ 364 | name: "add", 365 | description: "Add two numbers", 366 | parameters: z.object({ 367 | a: z.number(), 368 | b: z.number(), 369 | }), 370 | execute: async () => { 371 | return imageContent({ 372 | buffer: Buffer.from( 373 | "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", 374 | "base64", 375 | ), 376 | }); 377 | }, 378 | }); 379 | 380 | return server; 381 | }, 382 | run: async ({ client }) => { 383 | expect( 384 | await client.callTool({ 385 | name: "add", 386 | arguments: { 387 | a: 1, 388 | b: 2, 389 | }, 390 | }), 391 | ).toEqual({ 392 | content: [ 393 | { 394 | type: "image", 395 | data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", 396 | mimeType: "image/png", 397 | }, 398 | ], 399 | }); 400 | }, 401 | }); 402 | }); 403 | 404 | test("handles UserError errors", async () => { 405 | await runWithTestServer({ 406 | server: async () => { 407 | const server = new FastMCP({ 408 | name: "Test", 409 | version: "1.0.0", 410 | }); 411 | 412 | server.addTool({ 413 | name: "add", 414 | description: "Add two numbers", 415 | parameters: z.object({ 416 | a: z.number(), 417 | b: z.number(), 418 | }), 419 | execute: async () => { 420 | throw new UserError("Something went wrong"); 421 | }, 422 | }); 423 | 424 | return server; 425 | }, 426 | run: async ({ client }) => { 427 | expect( 428 | await client.callTool({ 429 | name: "add", 430 | arguments: { 431 | a: 1, 432 | b: 2, 433 | }, 434 | }), 435 | ).toEqual({ 436 | content: [{ type: "text", text: "Something went wrong" }], 437 | isError: true, 438 | }); 439 | }, 440 | }); 441 | }); 442 | 443 | test("calling an unknown tool throws McpError with MethodNotFound code", async () => { 444 | await runWithTestServer({ 445 | server: async () => { 446 | const server = new FastMCP({ 447 | name: "Test", 448 | version: "1.0.0", 449 | }); 450 | 451 | return server; 452 | }, 453 | run: async ({ client }) => { 454 | try { 455 | await client.callTool({ 456 | name: "add", 457 | arguments: { 458 | a: 1, 459 | b: 2, 460 | }, 461 | }); 462 | } catch (error) { 463 | expect(error).toBeInstanceOf(McpError); 464 | 465 | // @ts-expect-error - we know that error is an McpError 466 | expect(error.code).toBe(ErrorCode.MethodNotFound); 467 | } 468 | }, 469 | }); 470 | }); 471 | 472 | test("tracks tool progress", async () => { 473 | await runWithTestServer({ 474 | server: async () => { 475 | const server = new FastMCP({ 476 | name: "Test", 477 | version: "1.0.0", 478 | }); 479 | 480 | server.addTool({ 481 | name: "add", 482 | description: "Add two numbers", 483 | parameters: z.object({ 484 | a: z.number(), 485 | b: z.number(), 486 | }), 487 | execute: async (args, { reportProgress }) => { 488 | reportProgress({ 489 | progress: 0, 490 | total: 10, 491 | }); 492 | 493 | await delay(100); 494 | 495 | return String(args.a + args.b); 496 | }, 497 | }); 498 | 499 | return server; 500 | }, 501 | run: async ({ client }) => { 502 | const onProgress = vi.fn(); 503 | 504 | await client.callTool( 505 | { 506 | name: "add", 507 | arguments: { 508 | a: 1, 509 | b: 2, 510 | }, 511 | }, 512 | undefined, 513 | { 514 | onprogress: onProgress, 515 | }, 516 | ); 517 | 518 | expect(onProgress).toHaveBeenCalledTimes(1); 519 | expect(onProgress).toHaveBeenCalledWith({ 520 | progress: 0, 521 | total: 10, 522 | }); 523 | }, 524 | }); 525 | }); 526 | 527 | test("sets logging levels", async () => { 528 | await runWithTestServer({ 529 | run: async ({ client, session }) => { 530 | await client.setLoggingLevel("debug"); 531 | 532 | expect(session.loggingLevel).toBe("debug"); 533 | 534 | await client.setLoggingLevel("info"); 535 | 536 | expect(session.loggingLevel).toBe("info"); 537 | }, 538 | }); 539 | }); 540 | 541 | test("sends logging messages to the client", async () => { 542 | await runWithTestServer({ 543 | server: async () => { 544 | const server = new FastMCP({ 545 | name: "Test", 546 | version: "1.0.0", 547 | }); 548 | 549 | server.addTool({ 550 | name: "add", 551 | description: "Add two numbers", 552 | parameters: z.object({ 553 | a: z.number(), 554 | b: z.number(), 555 | }), 556 | execute: async (args, { log }) => { 557 | log.debug("debug message", { 558 | foo: "bar", 559 | }); 560 | log.error("error message"); 561 | log.info("info message"); 562 | log.warn("warn message"); 563 | 564 | return String(args.a + args.b); 565 | }, 566 | }); 567 | 568 | return server; 569 | }, 570 | run: async ({ client }) => { 571 | const onLog = vi.fn(); 572 | 573 | client.setNotificationHandler( 574 | LoggingMessageNotificationSchema, 575 | (message) => { 576 | if (message.method === "notifications/message") { 577 | onLog({ 578 | level: message.params.level, 579 | ...(message.params.data ?? {}), 580 | }); 581 | } 582 | }, 583 | ); 584 | 585 | await client.callTool({ 586 | name: "add", 587 | arguments: { 588 | a: 1, 589 | b: 2, 590 | }, 591 | }); 592 | 593 | expect(onLog).toHaveBeenCalledTimes(4); 594 | expect(onLog).toHaveBeenNthCalledWith(1, { 595 | level: "debug", 596 | message: "debug message", 597 | context: { 598 | foo: "bar", 599 | }, 600 | }); 601 | expect(onLog).toHaveBeenNthCalledWith(2, { 602 | level: "error", 603 | message: "error message", 604 | }); 605 | expect(onLog).toHaveBeenNthCalledWith(3, { 606 | level: "info", 607 | message: "info message", 608 | }); 609 | expect(onLog).toHaveBeenNthCalledWith(4, { 610 | level: "warning", 611 | message: "warn message", 612 | }); 613 | }, 614 | }); 615 | }); 616 | 617 | test("adds resources", async () => { 618 | await runWithTestServer({ 619 | server: async () => { 620 | const server = new FastMCP({ 621 | name: "Test", 622 | version: "1.0.0", 623 | }); 624 | 625 | server.addResource({ 626 | uri: "file:///logs/app.log", 627 | name: "Application Logs", 628 | mimeType: "text/plain", 629 | async load() { 630 | return { 631 | text: "Example log content", 632 | }; 633 | }, 634 | }); 635 | 636 | return server; 637 | }, 638 | run: async ({ client }) => { 639 | expect(await client.listResources()).toEqual({ 640 | resources: [ 641 | { 642 | uri: "file:///logs/app.log", 643 | name: "Application Logs", 644 | mimeType: "text/plain", 645 | }, 646 | ], 647 | }); 648 | }, 649 | }); 650 | }); 651 | 652 | test("clients reads a resource", async () => { 653 | await runWithTestServer({ 654 | server: async () => { 655 | const server = new FastMCP({ 656 | name: "Test", 657 | version: "1.0.0", 658 | }); 659 | 660 | server.addResource({ 661 | uri: "file:///logs/app.log", 662 | name: "Application Logs", 663 | mimeType: "text/plain", 664 | async load() { 665 | return { 666 | text: "Example log content", 667 | }; 668 | }, 669 | }); 670 | 671 | return server; 672 | }, 673 | run: async ({ client }) => { 674 | expect( 675 | await client.readResource({ 676 | uri: "file:///logs/app.log", 677 | }), 678 | ).toEqual({ 679 | contents: [ 680 | { 681 | uri: "file:///logs/app.log", 682 | name: "Application Logs", 683 | text: "Example log content", 684 | mimeType: "text/plain", 685 | }, 686 | ], 687 | }); 688 | }, 689 | }); 690 | }); 691 | 692 | test("clients reads a resource that returns multiple resources", async () => { 693 | await runWithTestServer({ 694 | server: async () => { 695 | const server = new FastMCP({ 696 | name: "Test", 697 | version: "1.0.0", 698 | }); 699 | 700 | server.addResource({ 701 | uri: "file:///logs/app.log", 702 | name: "Application Logs", 703 | mimeType: "text/plain", 704 | async load() { 705 | return [ 706 | { 707 | text: "a", 708 | }, 709 | { 710 | text: "b", 711 | }, 712 | ]; 713 | }, 714 | }); 715 | 716 | return server; 717 | }, 718 | run: async ({ client }) => { 719 | expect( 720 | await client.readResource({ 721 | uri: "file:///logs/app.log", 722 | }), 723 | ).toEqual({ 724 | contents: [ 725 | { 726 | uri: "file:///logs/app.log", 727 | name: "Application Logs", 728 | text: "a", 729 | mimeType: "text/plain", 730 | }, 731 | { 732 | uri: "file:///logs/app.log", 733 | name: "Application Logs", 734 | text: "b", 735 | mimeType: "text/plain", 736 | }, 737 | ], 738 | }); 739 | }, 740 | }); 741 | }); 742 | 743 | test("adds prompts", async () => { 744 | await runWithTestServer({ 745 | server: async () => { 746 | const server = new FastMCP({ 747 | name: "Test", 748 | version: "1.0.0", 749 | }); 750 | 751 | server.addPrompt({ 752 | name: "git-commit", 753 | description: "Generate a Git commit message", 754 | arguments: [ 755 | { 756 | name: "changes", 757 | description: "Git diff or description of changes", 758 | required: true, 759 | }, 760 | ], 761 | load: async (args) => { 762 | return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; 763 | }, 764 | }); 765 | 766 | return server; 767 | }, 768 | run: async ({ client }) => { 769 | expect( 770 | await client.getPrompt({ 771 | name: "git-commit", 772 | arguments: { 773 | changes: "foo", 774 | }, 775 | }), 776 | ).toEqual({ 777 | description: "Generate a Git commit message", 778 | messages: [ 779 | { 780 | role: "user", 781 | content: { 782 | type: "text", 783 | text: "Generate a concise but descriptive commit message for these changes:\n\nfoo", 784 | }, 785 | }, 786 | ], 787 | }); 788 | 789 | expect(await client.listPrompts()).toEqual({ 790 | prompts: [ 791 | { 792 | name: "git-commit", 793 | description: "Generate a Git commit message", 794 | arguments: [ 795 | { 796 | name: "changes", 797 | description: "Git diff or description of changes", 798 | required: true, 799 | }, 800 | ], 801 | }, 802 | ], 803 | }); 804 | }, 805 | }); 806 | }); 807 | 808 | test("uses events to notify server of client connect/disconnect", async () => { 809 | const port = await getRandomPort(); 810 | 811 | const server = new FastMCP({ 812 | name: "Test", 813 | version: "1.0.0", 814 | }); 815 | 816 | const onConnect = vi.fn(); 817 | const onDisconnect = vi.fn(); 818 | 819 | server.on("connect", onConnect); 820 | server.on("disconnect", onDisconnect); 821 | 822 | await server.start({ 823 | transportType: "sse", 824 | sse: { 825 | endpoint: "/sse", 826 | port, 827 | }, 828 | }); 829 | 830 | const client = new Client( 831 | { 832 | name: "example-client", 833 | version: "1.0.0", 834 | }, 835 | { 836 | capabilities: {}, 837 | }, 838 | ); 839 | 840 | const transport = new SSEClientTransport( 841 | new URL(`http://localhost:${port}/sse`), 842 | ); 843 | 844 | await client.connect(transport); 845 | 846 | await delay(100); 847 | 848 | expect(onConnect).toHaveBeenCalledTimes(1); 849 | expect(onDisconnect).toHaveBeenCalledTimes(0); 850 | 851 | expect(server.sessions).toEqual([expect.any(FastMCPSession)]); 852 | 853 | await client.close(); 854 | 855 | await delay(100); 856 | 857 | expect(onConnect).toHaveBeenCalledTimes(1); 858 | expect(onDisconnect).toHaveBeenCalledTimes(1); 859 | 860 | await server.stop(); 861 | }); 862 | 863 | test("handles multiple clients", async () => { 864 | const port = await getRandomPort(); 865 | 866 | const server = new FastMCP({ 867 | name: "Test", 868 | version: "1.0.0", 869 | }); 870 | 871 | await server.start({ 872 | transportType: "sse", 873 | sse: { 874 | endpoint: "/sse", 875 | port, 876 | }, 877 | }); 878 | 879 | const client1 = new Client( 880 | { 881 | name: "example-client", 882 | version: "1.0.0", 883 | }, 884 | { 885 | capabilities: {}, 886 | }, 887 | ); 888 | 889 | const transport1 = new SSEClientTransport( 890 | new URL(`http://localhost:${port}/sse`), 891 | ); 892 | 893 | await client1.connect(transport1); 894 | 895 | const client2 = new Client( 896 | { 897 | name: "example-client", 898 | version: "1.0.0", 899 | }, 900 | { 901 | capabilities: {}, 902 | }, 903 | ); 904 | 905 | const transport2 = new SSEClientTransport( 906 | new URL(`http://localhost:${port}/sse`), 907 | ); 908 | 909 | await client2.connect(transport2); 910 | 911 | await delay(100); 912 | 913 | expect(server.sessions).toEqual([ 914 | expect.any(FastMCPSession), 915 | expect.any(FastMCPSession), 916 | ]); 917 | 918 | await server.stop(); 919 | }); 920 | 921 | test("session knows about client capabilities", async () => { 922 | await runWithTestServer({ 923 | client: async () => { 924 | const client = new Client( 925 | { 926 | name: "example-client", 927 | version: "1.0.0", 928 | }, 929 | { 930 | capabilities: { 931 | roots: { 932 | listChanged: true, 933 | }, 934 | }, 935 | }, 936 | ); 937 | 938 | client.setRequestHandler(ListRootsRequestSchema, () => { 939 | return { 940 | roots: [ 941 | { 942 | uri: "file:///home/user/projects/frontend", 943 | name: "Frontend Repository", 944 | }, 945 | ], 946 | }; 947 | }); 948 | 949 | return client; 950 | }, 951 | run: async ({ session }) => { 952 | expect(session.clientCapabilities).toEqual({ 953 | roots: { 954 | listChanged: true, 955 | }, 956 | }); 957 | }, 958 | }); 959 | }); 960 | 961 | test("session knows about roots", async () => { 962 | await runWithTestServer({ 963 | client: async () => { 964 | const client = new Client( 965 | { 966 | name: "example-client", 967 | version: "1.0.0", 968 | }, 969 | { 970 | capabilities: { 971 | roots: { 972 | listChanged: true, 973 | }, 974 | }, 975 | }, 976 | ); 977 | 978 | client.setRequestHandler(ListRootsRequestSchema, () => { 979 | return { 980 | roots: [ 981 | { 982 | uri: "file:///home/user/projects/frontend", 983 | name: "Frontend Repository", 984 | }, 985 | ], 986 | }; 987 | }); 988 | 989 | return client; 990 | }, 991 | run: async ({ session }) => { 992 | expect(session.roots).toEqual([ 993 | { 994 | uri: "file:///home/user/projects/frontend", 995 | name: "Frontend Repository", 996 | }, 997 | ]); 998 | }, 999 | }); 1000 | }); 1001 | 1002 | test("session listens to roots changes", async () => { 1003 | let clientRoots: Root[] = [ 1004 | { 1005 | uri: "file:///home/user/projects/frontend", 1006 | name: "Frontend Repository", 1007 | }, 1008 | ]; 1009 | 1010 | await runWithTestServer({ 1011 | client: async () => { 1012 | const client = new Client( 1013 | { 1014 | name: "example-client", 1015 | version: "1.0.0", 1016 | }, 1017 | { 1018 | capabilities: { 1019 | roots: { 1020 | listChanged: true, 1021 | }, 1022 | }, 1023 | }, 1024 | ); 1025 | 1026 | client.setRequestHandler(ListRootsRequestSchema, () => { 1027 | return { 1028 | roots: clientRoots, 1029 | }; 1030 | }); 1031 | 1032 | return client; 1033 | }, 1034 | run: async ({ session, client }) => { 1035 | expect(session.roots).toEqual([ 1036 | { 1037 | uri: "file:///home/user/projects/frontend", 1038 | name: "Frontend Repository", 1039 | }, 1040 | ]); 1041 | 1042 | clientRoots.push({ 1043 | uri: "file:///home/user/projects/backend", 1044 | name: "Backend Repository", 1045 | }); 1046 | 1047 | await client.sendRootsListChanged(); 1048 | 1049 | const onRootsChanged = vi.fn(); 1050 | 1051 | session.on("rootsChanged", onRootsChanged); 1052 | 1053 | await delay(100); 1054 | 1055 | expect(session.roots).toEqual([ 1056 | { 1057 | uri: "file:///home/user/projects/frontend", 1058 | name: "Frontend Repository", 1059 | }, 1060 | { 1061 | uri: "file:///home/user/projects/backend", 1062 | name: "Backend Repository", 1063 | }, 1064 | ]); 1065 | 1066 | expect(onRootsChanged).toHaveBeenCalledTimes(1); 1067 | expect(onRootsChanged).toHaveBeenCalledWith({ 1068 | roots: [ 1069 | { 1070 | uri: "file:///home/user/projects/frontend", 1071 | name: "Frontend Repository", 1072 | }, 1073 | { 1074 | uri: "file:///home/user/projects/backend", 1075 | name: "Backend Repository", 1076 | }, 1077 | ], 1078 | }); 1079 | }, 1080 | }); 1081 | }); 1082 | 1083 | test("session sends pings to the client", async () => { 1084 | await runWithTestServer({ 1085 | run: async ({ client }) => { 1086 | const onPing = vi.fn().mockReturnValue({}); 1087 | 1088 | client.setRequestHandler(PingRequestSchema, onPing); 1089 | 1090 | await delay(2000); 1091 | 1092 | expect(onPing).toHaveBeenCalledTimes(1); 1093 | }, 1094 | }); 1095 | }); 1096 | 1097 | test("completes prompt arguments", async () => { 1098 | await runWithTestServer({ 1099 | server: async () => { 1100 | const server = new FastMCP({ 1101 | name: "Test", 1102 | version: "1.0.0", 1103 | }); 1104 | 1105 | server.addPrompt({ 1106 | name: "countryPoem", 1107 | description: "Writes a poem about a country", 1108 | load: async ({ name }) => { 1109 | return `Hello, ${name}!`; 1110 | }, 1111 | arguments: [ 1112 | { 1113 | name: "name", 1114 | description: "Name of the country", 1115 | required: true, 1116 | complete: async (value) => { 1117 | if (value === "Germ") { 1118 | return { 1119 | values: ["Germany"], 1120 | }; 1121 | } 1122 | 1123 | return { 1124 | values: [], 1125 | }; 1126 | }, 1127 | }, 1128 | ], 1129 | }); 1130 | 1131 | return server; 1132 | }, 1133 | run: async ({ client }) => { 1134 | const response = await client.complete({ 1135 | ref: { 1136 | type: "ref/prompt", 1137 | name: "countryPoem", 1138 | }, 1139 | argument: { 1140 | name: "name", 1141 | value: "Germ", 1142 | }, 1143 | }); 1144 | 1145 | expect(response).toEqual({ 1146 | completion: { 1147 | values: ["Germany"], 1148 | }, 1149 | }); 1150 | }, 1151 | }); 1152 | }); 1153 | 1154 | test("adds automatic prompt argument completion when enum is provided", async () => { 1155 | await runWithTestServer({ 1156 | server: async () => { 1157 | const server = new FastMCP({ 1158 | name: "Test", 1159 | version: "1.0.0", 1160 | }); 1161 | 1162 | server.addPrompt({ 1163 | name: "countryPoem", 1164 | description: "Writes a poem about a country", 1165 | load: async ({ name }) => { 1166 | return `Hello, ${name}!`; 1167 | }, 1168 | arguments: [ 1169 | { 1170 | name: "name", 1171 | description: "Name of the country", 1172 | required: true, 1173 | enum: ["Germany", "France", "Italy"], 1174 | }, 1175 | ], 1176 | }); 1177 | 1178 | return server; 1179 | }, 1180 | run: async ({ client }) => { 1181 | const response = await client.complete({ 1182 | ref: { 1183 | type: "ref/prompt", 1184 | name: "countryPoem", 1185 | }, 1186 | argument: { 1187 | name: "name", 1188 | value: "Germ", 1189 | }, 1190 | }); 1191 | 1192 | expect(response).toEqual({ 1193 | completion: { 1194 | values: ["Germany"], 1195 | total: 1, 1196 | }, 1197 | }); 1198 | }, 1199 | }); 1200 | }); 1201 | 1202 | test("completes template resource arguments", async () => { 1203 | await runWithTestServer({ 1204 | server: async () => { 1205 | const server = new FastMCP({ 1206 | name: "Test", 1207 | version: "1.0.0", 1208 | }); 1209 | 1210 | server.addResourceTemplate({ 1211 | uriTemplate: "issue:///{issueId}", 1212 | name: "Issue", 1213 | mimeType: "text/plain", 1214 | arguments: [ 1215 | { 1216 | name: "issueId", 1217 | description: "ID of the issue", 1218 | complete: async (value) => { 1219 | if (value === "123") { 1220 | return { 1221 | values: ["123456"], 1222 | }; 1223 | } 1224 | 1225 | return { 1226 | values: [], 1227 | }; 1228 | }, 1229 | }, 1230 | ], 1231 | load: async ({ issueId }) => { 1232 | return { 1233 | text: `Issue ${issueId}`, 1234 | }; 1235 | }, 1236 | }); 1237 | 1238 | return server; 1239 | }, 1240 | run: async ({ client }) => { 1241 | const response = await client.complete({ 1242 | ref: { 1243 | type: "ref/resource", 1244 | uri: "issue:///{issueId}", 1245 | }, 1246 | argument: { 1247 | name: "issueId", 1248 | value: "123", 1249 | }, 1250 | }); 1251 | 1252 | expect(response).toEqual({ 1253 | completion: { 1254 | values: ["123456"], 1255 | }, 1256 | }); 1257 | }, 1258 | }); 1259 | }); 1260 | 1261 | test("lists resource templates", async () => { 1262 | await runWithTestServer({ 1263 | server: async () => { 1264 | const server = new FastMCP({ 1265 | name: "Test", 1266 | version: "1.0.0", 1267 | }); 1268 | 1269 | server.addResourceTemplate({ 1270 | uriTemplate: "file:///logs/{name}.log", 1271 | name: "Application Logs", 1272 | mimeType: "text/plain", 1273 | arguments: [ 1274 | { 1275 | name: "name", 1276 | description: "Name of the log", 1277 | required: true, 1278 | }, 1279 | ], 1280 | load: async ({ name }) => { 1281 | return { 1282 | text: `Example log content for ${name}`, 1283 | }; 1284 | }, 1285 | }); 1286 | 1287 | return server; 1288 | }, 1289 | run: async ({ client }) => { 1290 | expect(await client.listResourceTemplates()).toEqual({ 1291 | resourceTemplates: [ 1292 | { 1293 | name: "Application Logs", 1294 | uriTemplate: "file:///logs/{name}.log", 1295 | }, 1296 | ], 1297 | }); 1298 | }, 1299 | }); 1300 | }); 1301 | 1302 | test("clients reads a resource accessed via a resource template", async () => { 1303 | const loadSpy = vi.fn((_args) => { 1304 | return { 1305 | text: "Example log content", 1306 | }; 1307 | }); 1308 | 1309 | await runWithTestServer({ 1310 | server: async () => { 1311 | const server = new FastMCP({ 1312 | name: "Test", 1313 | version: "1.0.0", 1314 | }); 1315 | 1316 | server.addResourceTemplate({ 1317 | uriTemplate: "file:///logs/{name}.log", 1318 | name: "Application Logs", 1319 | mimeType: "text/plain", 1320 | arguments: [ 1321 | { 1322 | name: "name", 1323 | description: "Name of the log", 1324 | }, 1325 | ], 1326 | async load(args) { 1327 | return loadSpy(args); 1328 | }, 1329 | }); 1330 | 1331 | return server; 1332 | }, 1333 | run: async ({ client }) => { 1334 | expect( 1335 | await client.readResource({ 1336 | uri: "file:///logs/app.log", 1337 | }), 1338 | ).toEqual({ 1339 | contents: [ 1340 | { 1341 | uri: "file:///logs/app.log", 1342 | name: "Application Logs", 1343 | text: "Example log content", 1344 | mimeType: "text/plain", 1345 | }, 1346 | ], 1347 | }); 1348 | 1349 | expect(loadSpy).toHaveBeenCalledWith({ 1350 | name: "app", 1351 | }); 1352 | }, 1353 | }); 1354 | }); 1355 | 1356 | test("makes a sampling request", async () => { 1357 | const onMessageRequest = vi.fn(() => { 1358 | return { 1359 | model: "gpt-3.5-turbo", 1360 | role: "assistant", 1361 | content: { 1362 | type: "text", 1363 | text: "The files are in the current directory.", 1364 | }, 1365 | }; 1366 | }); 1367 | 1368 | await runWithTestServer({ 1369 | client: async () => { 1370 | const client = new Client( 1371 | { 1372 | name: "example-client", 1373 | version: "1.0.0", 1374 | }, 1375 | { 1376 | capabilities: { 1377 | sampling: {}, 1378 | }, 1379 | }, 1380 | ); 1381 | return client; 1382 | }, 1383 | run: async ({ client, session }) => { 1384 | client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest); 1385 | 1386 | const response = await session.requestSampling({ 1387 | messages: [ 1388 | { 1389 | role: "user", 1390 | content: { 1391 | type: "text", 1392 | text: "What files are in the current directory?", 1393 | }, 1394 | }, 1395 | ], 1396 | systemPrompt: "You are a helpful file system assistant.", 1397 | includeContext: "thisServer", 1398 | maxTokens: 100, 1399 | }); 1400 | 1401 | expect(response).toEqual({ 1402 | model: "gpt-3.5-turbo", 1403 | role: "assistant", 1404 | content: { 1405 | type: "text", 1406 | text: "The files are in the current directory.", 1407 | }, 1408 | }); 1409 | 1410 | expect(onMessageRequest).toHaveBeenCalledTimes(1); 1411 | }, 1412 | }); 1413 | }); 1414 | 1415 | test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => { 1416 | await runWithTestServer({ 1417 | server: async () => { 1418 | const server = new FastMCP({ 1419 | name: "Test", 1420 | version: "1.0.0", 1421 | }); 1422 | 1423 | server.addTool({ 1424 | name: "add", 1425 | description: "Add two numbers", 1426 | parameters: z.object({ 1427 | a: z.number(), 1428 | b: z.number(), 1429 | }), 1430 | execute: async (args) => { 1431 | return String(args.a + args.b); 1432 | }, 1433 | }); 1434 | 1435 | return server; 1436 | }, 1437 | run: async ({ client }) => { 1438 | try { 1439 | await client.callTool({ 1440 | name: "add", 1441 | arguments: { 1442 | a: 1, 1443 | b: "invalid", 1444 | }, 1445 | }); 1446 | } catch (error) { 1447 | expect(error).toBeInstanceOf(McpError); 1448 | 1449 | // @ts-expect-error - we know that error is an McpError 1450 | expect(error.code).toBe(ErrorCode.InvalidParams); 1451 | 1452 | // @ts-expect-error - we know that error is an McpError 1453 | expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters"); 1454 | } 1455 | }, 1456 | }); 1457 | }); 1458 | 1459 | test("server remains usable after InvalidParams error", async () => { 1460 | await runWithTestServer({ 1461 | server: async () => { 1462 | const server = new FastMCP({ 1463 | name: "Test", 1464 | version: "1.0.0", 1465 | }); 1466 | 1467 | server.addTool({ 1468 | name: "add", 1469 | description: "Add two numbers", 1470 | parameters: z.object({ 1471 | a: z.number(), 1472 | b: z.number(), 1473 | }), 1474 | execute: async (args) => { 1475 | return String(args.a + args.b); 1476 | }, 1477 | }); 1478 | 1479 | return server; 1480 | }, 1481 | run: async ({ client }) => { 1482 | try { 1483 | await client.callTool({ 1484 | name: "add", 1485 | arguments: { 1486 | a: 1, 1487 | b: "invalid", 1488 | }, 1489 | }); 1490 | } catch (error) { 1491 | expect(error).toBeInstanceOf(McpError); 1492 | 1493 | // @ts-expect-error - we know that error is an McpError 1494 | expect(error.code).toBe(ErrorCode.InvalidParams); 1495 | 1496 | // @ts-expect-error - we know that error is an McpError 1497 | expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters"); 1498 | } 1499 | 1500 | expect( 1501 | await client.callTool({ 1502 | name: "add", 1503 | arguments: { 1504 | a: 1, 1505 | b: 2, 1506 | }, 1507 | }), 1508 | ).toEqual({ 1509 | content: [{ type: "text", text: "3" }], 1510 | }); 1511 | }, 1512 | }); 1513 | }); 1514 | 1515 | test("allows new clients to connect after a client disconnects", async () => { 1516 | const port = await getRandomPort(); 1517 | 1518 | const server = new FastMCP({ 1519 | name: "Test", 1520 | version: "1.0.0", 1521 | }); 1522 | 1523 | server.addTool({ 1524 | name: "add", 1525 | description: "Add two numbers", 1526 | parameters: z.object({ 1527 | a: z.number(), 1528 | b: z.number(), 1529 | }), 1530 | execute: async (args) => { 1531 | return String(args.a + args.b); 1532 | }, 1533 | }); 1534 | 1535 | await server.start({ 1536 | transportType: "sse", 1537 | sse: { 1538 | endpoint: "/sse", 1539 | port, 1540 | }, 1541 | }); 1542 | 1543 | const client1 = new Client( 1544 | { 1545 | name: "example-client", 1546 | version: "1.0.0", 1547 | }, 1548 | { 1549 | capabilities: {}, 1550 | }, 1551 | ); 1552 | 1553 | const transport1 = new SSEClientTransport( 1554 | new URL(`http://localhost:${port}/sse`), 1555 | ); 1556 | 1557 | await client1.connect(transport1); 1558 | 1559 | expect( 1560 | await client1.callTool({ 1561 | name: "add", 1562 | arguments: { 1563 | a: 1, 1564 | b: 2, 1565 | }, 1566 | }), 1567 | ).toEqual({ 1568 | content: [{ type: "text", text: "3" }], 1569 | }); 1570 | 1571 | await client1.close(); 1572 | 1573 | const client2 = new Client( 1574 | { 1575 | name: "example-client", 1576 | version: "1.0.0", 1577 | }, 1578 | { 1579 | capabilities: {}, 1580 | }, 1581 | ); 1582 | 1583 | const transport2 = new SSEClientTransport( 1584 | new URL(`http://localhost:${port}/sse`), 1585 | ); 1586 | 1587 | await client2.connect(transport2); 1588 | 1589 | expect( 1590 | await client2.callTool({ 1591 | name: "add", 1592 | arguments: { 1593 | a: 1, 1594 | b: 2, 1595 | }, 1596 | }), 1597 | ).toEqual({ 1598 | content: [{ type: "text", text: "3" }], 1599 | }); 1600 | 1601 | await client2.close(); 1602 | 1603 | await server.stop(); 1604 | }); 1605 | 1606 | test("able to close server immediately after starting it", async () => { 1607 | const port = await getRandomPort(); 1608 | 1609 | const server = new FastMCP({ 1610 | name: "Test", 1611 | version: "1.0.0", 1612 | }); 1613 | 1614 | await server.start({ 1615 | transportType: "sse", 1616 | sse: { 1617 | endpoint: "/sse", 1618 | port, 1619 | }, 1620 | }); 1621 | 1622 | // We were previously not waiting for the server to start. 1623 | // Therefore, this would have caused error 'Server is not running.'. 1624 | await server.stop(); 1625 | }); 1626 | 1627 | test("closing event source does not produce error", async () => { 1628 | const port = await getRandomPort(); 1629 | 1630 | const server = new FastMCP({ 1631 | name: "Test", 1632 | version: "1.0.0", 1633 | }); 1634 | 1635 | server.addTool({ 1636 | name: "add", 1637 | description: "Add two numbers", 1638 | parameters: z.object({ 1639 | a: z.number(), 1640 | b: z.number(), 1641 | }), 1642 | execute: async (args) => { 1643 | return String(args.a + args.b); 1644 | }, 1645 | }); 1646 | 1647 | await server.start({ 1648 | transportType: "sse", 1649 | sse: { 1650 | endpoint: "/sse", 1651 | port, 1652 | }, 1653 | }); 1654 | 1655 | const eventSource = await new Promise<EventSourceClient>((onMessage) => { 1656 | const eventSource = createEventSource({ 1657 | onConnect: () => { 1658 | console.info('connected'); 1659 | }, 1660 | onDisconnect: () => { 1661 | console.info('disconnected'); 1662 | }, 1663 | onMessage: () => { 1664 | onMessage(eventSource); 1665 | }, 1666 | url: `http://127.0.0.1:${port}/sse`, 1667 | }); 1668 | }); 1669 | 1670 | expect(eventSource.readyState).toBe('open'); 1671 | 1672 | eventSource.close(); 1673 | 1674 | // We were getting unhandled error 'Not connected' 1675 | // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52 1676 | await delay(1000); 1677 | 1678 | await server.stop(); 1679 | }); 1680 | 1681 | test("provides auth to tools", async () => { 1682 | const port = await getRandomPort(); 1683 | 1684 | const authenticate = vi.fn(async () => { 1685 | return { 1686 | id: 1, 1687 | }; 1688 | }); 1689 | 1690 | const server = new FastMCP<{id: number}>({ 1691 | name: "Test", 1692 | version: "1.0.0", 1693 | authenticate, 1694 | }); 1695 | 1696 | const execute = vi.fn(async (args) => { 1697 | return String(args.a + args.b); 1698 | }); 1699 | 1700 | server.addTool({ 1701 | name: "add", 1702 | description: "Add two numbers", 1703 | parameters: z.object({ 1704 | a: z.number(), 1705 | b: z.number(), 1706 | }), 1707 | execute, 1708 | }); 1709 | 1710 | await server.start({ 1711 | transportType: "sse", 1712 | sse: { 1713 | endpoint: "/sse", 1714 | port, 1715 | }, 1716 | }); 1717 | 1718 | const client = new Client( 1719 | { 1720 | name: "example-client", 1721 | version: "1.0.0", 1722 | }, 1723 | { 1724 | capabilities: {}, 1725 | }, 1726 | ); 1727 | 1728 | const transport = new SSEClientTransport( 1729 | new URL(`http://localhost:${port}/sse`), 1730 | { 1731 | eventSourceInit: { 1732 | fetch: async (url, init) => { 1733 | return fetch(url, { 1734 | ...init, 1735 | headers: { 1736 | ...init?.headers, 1737 | "x-api-key": "123", 1738 | }, 1739 | }); 1740 | }, 1741 | }, 1742 | }, 1743 | ); 1744 | 1745 | await client.connect(transport); 1746 | 1747 | expect(authenticate, "authenticate should have been called").toHaveBeenCalledTimes(1); 1748 | 1749 | expect( 1750 | await client.callTool({ 1751 | name: "add", 1752 | arguments: { 1753 | a: 1, 1754 | b: 2, 1755 | }, 1756 | }), 1757 | ).toEqual({ 1758 | content: [{ type: "text", text: "3" }], 1759 | }); 1760 | 1761 | expect(execute, "execute should have been called").toHaveBeenCalledTimes(1); 1762 | 1763 | expect(execute).toHaveBeenCalledWith({ 1764 | a: 1, 1765 | b: 2, 1766 | }, { 1767 | log: { 1768 | debug: expect.any(Function), 1769 | error: expect.any(Function), 1770 | info: expect.any(Function), 1771 | warn: expect.any(Function), 1772 | }, 1773 | reportProgress: expect.any(Function), 1774 | session: { id: 1 }, 1775 | }); 1776 | }); 1777 | 1778 | test("blocks unauthorized requests", async () => { 1779 | const port = await getRandomPort(); 1780 | 1781 | const server = new FastMCP<{id: number}>({ 1782 | name: "Test", 1783 | version: "1.0.0", 1784 | authenticate: async () => { 1785 | throw new Response(null, { 1786 | status: 401, 1787 | statusText: "Unauthorized", 1788 | }); 1789 | }, 1790 | }); 1791 | 1792 | await server.start({ 1793 | transportType: "sse", 1794 | sse: { 1795 | endpoint: "/sse", 1796 | port, 1797 | }, 1798 | }); 1799 | 1800 | const client = new Client( 1801 | { 1802 | name: "example-client", 1803 | version: "1.0.0", 1804 | }, 1805 | { 1806 | capabilities: {}, 1807 | }, 1808 | ); 1809 | 1810 | const transport = new SSEClientTransport( 1811 | new URL(`http://localhost:${port}/sse`), 1812 | ); 1813 | 1814 | expect(async () => { 1815 | await client.connect(transport); 1816 | }).rejects.toThrow("SSE error: Non-200 status code (401)"); 1817 | }); 1818 | 1819 | 1820 | --- 1821 | File: /src/FastMCP.ts 1822 | --- 1823 | 1824 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 1825 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 1826 | import { 1827 | CallToolRequestSchema, 1828 | ClientCapabilities, 1829 | CompleteRequestSchema, 1830 | CreateMessageRequestSchema, 1831 | ErrorCode, 1832 | GetPromptRequestSchema, 1833 | ListPromptsRequestSchema, 1834 | ListResourcesRequestSchema, 1835 | ListResourceTemplatesRequestSchema, 1836 | ListToolsRequestSchema, 1837 | McpError, 1838 | ReadResourceRequestSchema, 1839 | Root, 1840 | RootsListChangedNotificationSchema, 1841 | ServerCapabilities, 1842 | SetLevelRequestSchema, 1843 | } from "@modelcontextprotocol/sdk/types.js"; 1844 | import { zodToJsonSchema } from "zod-to-json-schema"; 1845 | import { z } from "zod"; 1846 | import { setTimeout as delay } from "timers/promises"; 1847 | import { readFile } from "fs/promises"; 1848 | import { fileTypeFromBuffer } from "file-type"; 1849 | import { StrictEventEmitter } from "strict-event-emitter-types"; 1850 | import { EventEmitter } from "events"; 1851 | import Fuse from "fuse.js"; 1852 | import { startSSEServer } from "mcp-proxy"; 1853 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; 1854 | import parseURITemplate from "uri-templates"; 1855 | import http from "http"; 1856 | import { 1857 | fetch 1858 | } from "undici"; 1859 | 1860 | export type SSEServer = { 1861 | close: () => Promise<void>; 1862 | }; 1863 | 1864 | type FastMCPEvents<T extends FastMCPSessionAuth> = { 1865 | connect: (event: { session: FastMCPSession<T> }) => void; 1866 | disconnect: (event: { session: FastMCPSession<T> }) => void; 1867 | }; 1868 | 1869 | type FastMCPSessionEvents = { 1870 | rootsChanged: (event: { roots: Root[] }) => void; 1871 | error: (event: { error: Error }) => void; 1872 | }; 1873 | 1874 | /** 1875 | * Generates an image content object from a URL, file path, or buffer. 1876 | */ 1877 | export const imageContent = async ( 1878 | input: { url: string } | { path: string } | { buffer: Buffer }, 1879 | ): Promise<ImageContent> => { 1880 | let rawData: Buffer; 1881 | 1882 | if ("url" in input) { 1883 | const response = await fetch(input.url); 1884 | 1885 | if (!response.ok) { 1886 | throw new Error(`Failed to fetch image from URL: ${response.statusText}`); 1887 | } 1888 | 1889 | rawData = Buffer.from(await response.arrayBuffer()); 1890 | } else if ("path" in input) { 1891 | rawData = await readFile(input.path); 1892 | } else if ("buffer" in input) { 1893 | rawData = input.buffer; 1894 | } else { 1895 | throw new Error( 1896 | "Invalid input: Provide a valid 'url', 'path', or 'buffer'", 1897 | ); 1898 | } 1899 | 1900 | const mimeType = await fileTypeFromBuffer(rawData); 1901 | 1902 | const base64Data = rawData.toString("base64"); 1903 | 1904 | return { 1905 | type: "image", 1906 | data: base64Data, 1907 | mimeType: mimeType?.mime ?? "image/png", 1908 | } as const; 1909 | }; 1910 | 1911 | abstract class FastMCPError extends Error { 1912 | public constructor(message?: string) { 1913 | super(message); 1914 | this.name = new.target.name; 1915 | } 1916 | } 1917 | 1918 | type Extra = unknown; 1919 | 1920 | type Extras = Record<string, Extra>; 1921 | 1922 | export class UnexpectedStateError extends FastMCPError { 1923 | public extras?: Extras; 1924 | 1925 | public constructor(message: string, extras?: Extras) { 1926 | super(message); 1927 | this.name = new.target.name; 1928 | this.extras = extras; 1929 | } 1930 | } 1931 | 1932 | /** 1933 | * An error that is meant to be surfaced to the user. 1934 | */ 1935 | export class UserError extends UnexpectedStateError {} 1936 | 1937 | type ToolParameters = z.ZodTypeAny; 1938 | 1939 | type Literal = boolean | null | number | string | undefined; 1940 | 1941 | type SerializableValue = 1942 | | Literal 1943 | | SerializableValue[] 1944 | | { [key: string]: SerializableValue }; 1945 | 1946 | type Progress = { 1947 | /** 1948 | * The progress thus far. This should increase every time progress is made, even if the total is unknown. 1949 | */ 1950 | progress: number; 1951 | /** 1952 | * Total number of items to process (or total progress required), if known. 1953 | */ 1954 | total?: number; 1955 | }; 1956 | 1957 | type Context<T extends FastMCPSessionAuth> = { 1958 | session: T | undefined; 1959 | reportProgress: (progress: Progress) => Promise<void>; 1960 | log: { 1961 | debug: (message: string, data?: SerializableValue) => void; 1962 | error: (message: string, data?: SerializableValue) => void; 1963 | info: (message: string, data?: SerializableValue) => void; 1964 | warn: (message: string, data?: SerializableValue) => void; 1965 | }; 1966 | }; 1967 | 1968 | type TextContent = { 1969 | type: "text"; 1970 | text: string; 1971 | }; 1972 | 1973 | const TextContentZodSchema = z 1974 | .object({ 1975 | type: z.literal("text"), 1976 | /** 1977 | * The text content of the message. 1978 | */ 1979 | text: z.string(), 1980 | }) 1981 | .strict() satisfies z.ZodType<TextContent>; 1982 | 1983 | type ImageContent = { 1984 | type: "image"; 1985 | data: string; 1986 | mimeType: string; 1987 | }; 1988 | 1989 | const ImageContentZodSchema = z 1990 | .object({ 1991 | type: z.literal("image"), 1992 | /** 1993 | * The base64-encoded image data. 1994 | */ 1995 | data: z.string().base64(), 1996 | /** 1997 | * The MIME type of the image. Different providers may support different image types. 1998 | */ 1999 | mimeType: z.string(), 2000 | }) 2001 | .strict() satisfies z.ZodType<ImageContent>; 2002 | 2003 | type Content = TextContent | ImageContent; 2004 | 2005 | const ContentZodSchema = z.discriminatedUnion("type", [ 2006 | TextContentZodSchema, 2007 | ImageContentZodSchema, 2008 | ]) satisfies z.ZodType<Content>; 2009 | 2010 | type ContentResult = { 2011 | content: Content[]; 2012 | isError?: boolean; 2013 | }; 2014 | 2015 | const ContentResultZodSchema = z 2016 | .object({ 2017 | content: ContentZodSchema.array(), 2018 | isError: z.boolean().optional(), 2019 | }) 2020 | .strict() satisfies z.ZodType<ContentResult>; 2021 | 2022 | type Completion = { 2023 | values: string[]; 2024 | total?: number; 2025 | hasMore?: boolean; 2026 | }; 2027 | 2028 | /** 2029 | * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 2030 | */ 2031 | const CompletionZodSchema = z.object({ 2032 | /** 2033 | * An array of completion values. Must not exceed 100 items. 2034 | */ 2035 | values: z.array(z.string()).max(100), 2036 | /** 2037 | * The total number of completion options available. This can exceed the number of values actually sent in the response. 2038 | */ 2039 | total: z.optional(z.number().int()), 2040 | /** 2041 | * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. 2042 | */ 2043 | hasMore: z.optional(z.boolean()), 2044 | }) satisfies z.ZodType<Completion>; 2045 | 2046 | type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = { 2047 | name: string; 2048 | description?: string; 2049 | parameters?: Params; 2050 | execute: ( 2051 | args: z.infer<Params>, 2052 | context: Context<T>, 2053 | ) => Promise<string | ContentResult | TextContent | ImageContent>; 2054 | }; 2055 | 2056 | type ResourceResult = 2057 | | { 2058 | text: string; 2059 | } 2060 | | { 2061 | blob: string; 2062 | }; 2063 | 2064 | type InputResourceTemplateArgument = Readonly<{ 2065 | name: string; 2066 | description?: string; 2067 | complete?: ArgumentValueCompleter; 2068 | }>; 2069 | 2070 | type ResourceTemplateArgument = Readonly<{ 2071 | name: string; 2072 | description?: string; 2073 | complete?: ArgumentValueCompleter; 2074 | }>; 2075 | 2076 | type ResourceTemplate< 2077 | Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], 2078 | > = { 2079 | uriTemplate: string; 2080 | name: string; 2081 | description?: string; 2082 | mimeType?: string; 2083 | arguments: Arguments; 2084 | complete?: (name: string, value: string) => Promise<Completion>; 2085 | load: ( 2086 | args: ResourceTemplateArgumentsToObject<Arguments>, 2087 | ) => Promise<ResourceResult>; 2088 | }; 2089 | 2090 | type ResourceTemplateArgumentsToObject<T extends { name: string }[]> = { 2091 | [K in T[number]["name"]]: string; 2092 | }; 2093 | 2094 | type InputResourceTemplate< 2095 | Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], 2096 | > = { 2097 | uriTemplate: string; 2098 | name: string; 2099 | description?: string; 2100 | mimeType?: string; 2101 | arguments: Arguments; 2102 | load: ( 2103 | args: ResourceTemplateArgumentsToObject<Arguments>, 2104 | ) => Promise<ResourceResult>; 2105 | }; 2106 | 2107 | type Resource = { 2108 | uri: string; 2109 | name: string; 2110 | description?: string; 2111 | mimeType?: string; 2112 | load: () => Promise<ResourceResult | ResourceResult[]>; 2113 | complete?: (name: string, value: string) => Promise<Completion>; 2114 | }; 2115 | 2116 | type ArgumentValueCompleter = (value: string) => Promise<Completion>; 2117 | 2118 | type InputPromptArgument = Readonly<{ 2119 | name: string; 2120 | description?: string; 2121 | required?: boolean; 2122 | complete?: ArgumentValueCompleter; 2123 | enum?: string[]; 2124 | }>; 2125 | 2126 | type PromptArgumentsToObject<T extends { name: string; required?: boolean }[]> = 2127 | { 2128 | [K in T[number]["name"]]: Extract< 2129 | T[number], 2130 | { name: K } 2131 | >["required"] extends true 2132 | ? string 2133 | : string | undefined; 2134 | }; 2135 | 2136 | type InputPrompt< 2137 | Arguments extends InputPromptArgument[] = InputPromptArgument[], 2138 | Args = PromptArgumentsToObject<Arguments>, 2139 | > = { 2140 | name: string; 2141 | description?: string; 2142 | arguments?: InputPromptArgument[]; 2143 | load: (args: Args) => Promise<string>; 2144 | }; 2145 | 2146 | type PromptArgument = Readonly<{ 2147 | name: string; 2148 | description?: string; 2149 | required?: boolean; 2150 | complete?: ArgumentValueCompleter; 2151 | enum?: string[]; 2152 | }>; 2153 | 2154 | type Prompt< 2155 | Arguments extends PromptArgument[] = PromptArgument[], 2156 | Args = PromptArgumentsToObject<Arguments>, 2157 | > = { 2158 | arguments?: PromptArgument[]; 2159 | complete?: (name: string, value: string) => Promise<Completion>; 2160 | description?: string; 2161 | load: (args: Args) => Promise<string>; 2162 | name: string; 2163 | }; 2164 | 2165 | type ServerOptions<T extends FastMCPSessionAuth> = { 2166 | name: string; 2167 | version: `${number}.${number}.${number}`; 2168 | authenticate?: Authenticate<T>; 2169 | }; 2170 | 2171 | type LoggingLevel = 2172 | | "debug" 2173 | | "info" 2174 | | "notice" 2175 | | "warning" 2176 | | "error" 2177 | | "critical" 2178 | | "alert" 2179 | | "emergency"; 2180 | 2181 | const FastMCPSessionEventEmitterBase: { 2182 | new (): StrictEventEmitter<EventEmitter, FastMCPSessionEvents>; 2183 | } = EventEmitter; 2184 | 2185 | class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} 2186 | 2187 | type SamplingResponse = { 2188 | model: string; 2189 | stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; 2190 | role: "user" | "assistant"; 2191 | content: TextContent | ImageContent; 2192 | }; 2193 | 2194 | type FastMCPSessionAuth = Record<string, unknown> | undefined; 2195 | 2196 | export class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends FastMCPSessionEventEmitter { 2197 | #capabilities: ServerCapabilities = {}; 2198 | #clientCapabilities?: ClientCapabilities; 2199 | #loggingLevel: LoggingLevel = "info"; 2200 | #prompts: Prompt[] = []; 2201 | #resources: Resource[] = []; 2202 | #resourceTemplates: ResourceTemplate[] = []; 2203 | #roots: Root[] = []; 2204 | #server: Server; 2205 | #auth: T | undefined; 2206 | 2207 | constructor({ 2208 | auth, 2209 | name, 2210 | version, 2211 | tools, 2212 | resources, 2213 | resourcesTemplates, 2214 | prompts, 2215 | }: { 2216 | auth?: T; 2217 | name: string; 2218 | version: string; 2219 | tools: Tool<T>[]; 2220 | resources: Resource[]; 2221 | resourcesTemplates: InputResourceTemplate[]; 2222 | prompts: Prompt[]; 2223 | }) { 2224 | super(); 2225 | 2226 | this.#auth = auth; 2227 | 2228 | if (tools.length) { 2229 | this.#capabilities.tools = {}; 2230 | } 2231 | 2232 | if (resources.length || resourcesTemplates.length) { 2233 | this.#capabilities.resources = {}; 2234 | } 2235 | 2236 | if (prompts.length) { 2237 | for (const prompt of prompts) { 2238 | this.addPrompt(prompt); 2239 | } 2240 | 2241 | this.#capabilities.prompts = {}; 2242 | } 2243 | 2244 | this.#capabilities.logging = {}; 2245 | 2246 | this.#server = new Server( 2247 | { name: name, version: version }, 2248 | { capabilities: this.#capabilities }, 2249 | ); 2250 | 2251 | this.setupErrorHandling(); 2252 | this.setupLoggingHandlers(); 2253 | this.setupRootsHandlers(); 2254 | this.setupCompleteHandlers(); 2255 | 2256 | if (tools.length) { 2257 | this.setupToolHandlers(tools); 2258 | } 2259 | 2260 | if (resources.length || resourcesTemplates.length) { 2261 | for (const resource of resources) { 2262 | this.addResource(resource); 2263 | } 2264 | 2265 | this.setupResourceHandlers(resources); 2266 | 2267 | if (resourcesTemplates.length) { 2268 | for (const resourceTemplate of resourcesTemplates) { 2269 | this.addResourceTemplate(resourceTemplate); 2270 | } 2271 | 2272 | this.setupResourceTemplateHandlers(resourcesTemplates); 2273 | } 2274 | } 2275 | 2276 | if (prompts.length) { 2277 | this.setupPromptHandlers(prompts); 2278 | } 2279 | } 2280 | 2281 | private addResource(inputResource: Resource) { 2282 | this.#resources.push(inputResource); 2283 | } 2284 | 2285 | private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) { 2286 | const completers: Record<string, ArgumentValueCompleter> = {}; 2287 | 2288 | for (const argument of inputResourceTemplate.arguments ?? []) { 2289 | if (argument.complete) { 2290 | completers[argument.name] = argument.complete; 2291 | } 2292 | } 2293 | 2294 | const resourceTemplate = { 2295 | ...inputResourceTemplate, 2296 | complete: async (name: string, value: string) => { 2297 | if (completers[name]) { 2298 | return await completers[name](value); 2299 | } 2300 | 2301 | return { 2302 | values: [], 2303 | }; 2304 | }, 2305 | }; 2306 | 2307 | this.#resourceTemplates.push(resourceTemplate); 2308 | } 2309 | 2310 | private addPrompt(inputPrompt: InputPrompt) { 2311 | const completers: Record<string, ArgumentValueCompleter> = {}; 2312 | const enums: Record<string, string[]> = {}; 2313 | 2314 | for (const argument of inputPrompt.arguments ?? []) { 2315 | if (argument.complete) { 2316 | completers[argument.name] = argument.complete; 2317 | } 2318 | 2319 | if (argument.enum) { 2320 | enums[argument.name] = argument.enum; 2321 | } 2322 | } 2323 | 2324 | const prompt = { 2325 | ...inputPrompt, 2326 | complete: async (name: string, value: string) => { 2327 | if (completers[name]) { 2328 | return await completers[name](value); 2329 | } 2330 | 2331 | if (enums[name]) { 2332 | const fuse = new Fuse(enums[name], { 2333 | keys: ["value"], 2334 | }); 2335 | 2336 | const result = fuse.search(value); 2337 | 2338 | return { 2339 | values: result.map((item) => item.item), 2340 | total: result.length, 2341 | }; 2342 | } 2343 | 2344 | return { 2345 | values: [], 2346 | }; 2347 | }, 2348 | }; 2349 | 2350 | this.#prompts.push(prompt); 2351 | } 2352 | 2353 | public get clientCapabilities(): ClientCapabilities | null { 2354 | return this.#clientCapabilities ?? null; 2355 | } 2356 | 2357 | public get server(): Server { 2358 | return this.#server; 2359 | } 2360 | 2361 | #pingInterval: ReturnType<typeof setInterval> | null = null; 2362 | 2363 | public async requestSampling( 2364 | message: z.infer<typeof CreateMessageRequestSchema>["params"], 2365 | ): Promise<SamplingResponse> { 2366 | return this.#server.createMessage(message); 2367 | } 2368 | 2369 | public async connect(transport: Transport) { 2370 | if (this.#server.transport) { 2371 | throw new UnexpectedStateError("Server is already connected"); 2372 | } 2373 | 2374 | await this.#server.connect(transport); 2375 | 2376 | let attempt = 0; 2377 | 2378 | while (attempt++ < 10) { 2379 | const capabilities = await this.#server.getClientCapabilities(); 2380 | 2381 | if (capabilities) { 2382 | this.#clientCapabilities = capabilities; 2383 | 2384 | break; 2385 | } 2386 | 2387 | await delay(100); 2388 | } 2389 | 2390 | if (!this.#clientCapabilities) { 2391 | console.warn('[warning] FastMCP could not infer client capabilities') 2392 | } 2393 | 2394 | if (this.#clientCapabilities?.roots?.listChanged) { 2395 | try { 2396 | const roots = await this.#server.listRoots(); 2397 | this.#roots = roots.roots; 2398 | } catch(e) { 2399 | console.error(`[error] FastMCP received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`) 2400 | } 2401 | } 2402 | 2403 | this.#pingInterval = setInterval(async () => { 2404 | try { 2405 | await this.#server.ping(); 2406 | } catch (error) { 2407 | this.emit("error", { 2408 | error: error as Error, 2409 | }); 2410 | } 2411 | }, 1000); 2412 | } 2413 | 2414 | public get roots(): Root[] { 2415 | return this.#roots; 2416 | } 2417 | 2418 | public async close() { 2419 | if (this.#pingInterval) { 2420 | clearInterval(this.#pingInterval); 2421 | } 2422 | 2423 | try { 2424 | await this.#server.close(); 2425 | } catch (error) { 2426 | console.error("[MCP Error]", "could not close server", error); 2427 | } 2428 | } 2429 | 2430 | private setupErrorHandling() { 2431 | this.#server.onerror = (error) => { 2432 | console.error("[MCP Error]", error); 2433 | }; 2434 | } 2435 | 2436 | public get loggingLevel(): LoggingLevel { 2437 | return this.#loggingLevel; 2438 | } 2439 | 2440 | private setupCompleteHandlers() { 2441 | this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { 2442 | if (request.params.ref.type === "ref/prompt") { 2443 | const prompt = this.#prompts.find( 2444 | (prompt) => prompt.name === request.params.ref.name, 2445 | ); 2446 | 2447 | if (!prompt) { 2448 | throw new UnexpectedStateError("Unknown prompt", { 2449 | request, 2450 | }); 2451 | } 2452 | 2453 | if (!prompt.complete) { 2454 | throw new UnexpectedStateError("Prompt does not support completion", { 2455 | request, 2456 | }); 2457 | } 2458 | 2459 | const completion = CompletionZodSchema.parse( 2460 | await prompt.complete( 2461 | request.params.argument.name, 2462 | request.params.argument.value, 2463 | ), 2464 | ); 2465 | 2466 | return { 2467 | completion, 2468 | }; 2469 | } 2470 | 2471 | if (request.params.ref.type === "ref/resource") { 2472 | const resource = this.#resourceTemplates.find( 2473 | (resource) => resource.uriTemplate === request.params.ref.uri, 2474 | ); 2475 | 2476 | if (!resource) { 2477 | throw new UnexpectedStateError("Unknown resource", { 2478 | request, 2479 | }); 2480 | } 2481 | 2482 | if (!("uriTemplate" in resource)) { 2483 | throw new UnexpectedStateError("Unexpected resource"); 2484 | } 2485 | 2486 | if (!resource.complete) { 2487 | throw new UnexpectedStateError( 2488 | "Resource does not support completion", 2489 | { 2490 | request, 2491 | }, 2492 | ); 2493 | } 2494 | 2495 | const completion = CompletionZodSchema.parse( 2496 | await resource.complete( 2497 | request.params.argument.name, 2498 | request.params.argument.value, 2499 | ), 2500 | ); 2501 | 2502 | return { 2503 | completion, 2504 | }; 2505 | } 2506 | 2507 | throw new UnexpectedStateError("Unexpected completion request", { 2508 | request, 2509 | }); 2510 | }); 2511 | } 2512 | 2513 | private setupRootsHandlers() { 2514 | this.#server.setNotificationHandler( 2515 | RootsListChangedNotificationSchema, 2516 | () => { 2517 | this.#server.listRoots().then((roots) => { 2518 | this.#roots = roots.roots; 2519 | 2520 | this.emit("rootsChanged", { 2521 | roots: roots.roots, 2522 | }); 2523 | }); 2524 | }, 2525 | ); 2526 | } 2527 | 2528 | private setupLoggingHandlers() { 2529 | this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { 2530 | this.#loggingLevel = request.params.level; 2531 | 2532 | return {}; 2533 | }); 2534 | } 2535 | 2536 | private setupToolHandlers(tools: Tool<T>[]) { 2537 | this.#server.setRequestHandler(ListToolsRequestSchema, async () => { 2538 | return { 2539 | tools: tools.map((tool) => { 2540 | return { 2541 | name: tool.name, 2542 | description: tool.description, 2543 | inputSchema: tool.parameters 2544 | ? zodToJsonSchema(tool.parameters) 2545 | : undefined, 2546 | }; 2547 | }), 2548 | }; 2549 | }); 2550 | 2551 | this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { 2552 | const tool = tools.find((tool) => tool.name === request.params.name); 2553 | 2554 | if (!tool) { 2555 | throw new McpError( 2556 | ErrorCode.MethodNotFound, 2557 | `Unknown tool: ${request.params.name}`, 2558 | ); 2559 | } 2560 | 2561 | let args: any = undefined; 2562 | 2563 | if (tool.parameters) { 2564 | const parsed = tool.parameters.safeParse(request.params.arguments); 2565 | 2566 | if (!parsed.success) { 2567 | throw new McpError( 2568 | ErrorCode.InvalidParams, 2569 | `Invalid ${request.params.name} parameters`, 2570 | ); 2571 | } 2572 | 2573 | args = parsed.data; 2574 | } 2575 | 2576 | const progressToken = request.params?._meta?.progressToken; 2577 | 2578 | let result: ContentResult; 2579 | 2580 | try { 2581 | const reportProgress = async (progress: Progress) => { 2582 | await this.#server.notification({ 2583 | method: "notifications/progress", 2584 | params: { 2585 | ...progress, 2586 | progressToken, 2587 | }, 2588 | }); 2589 | }; 2590 | 2591 | const log = { 2592 | debug: (message: string, context?: SerializableValue) => { 2593 | this.#server.sendLoggingMessage({ 2594 | level: "debug", 2595 | data: { 2596 | message, 2597 | context, 2598 | }, 2599 | }); 2600 | }, 2601 | error: (message: string, context?: SerializableValue) => { 2602 | this.#server.sendLoggingMessage({ 2603 | level: "error", 2604 | data: { 2605 | message, 2606 | context, 2607 | }, 2608 | }); 2609 | }, 2610 | info: (message: string, context?: SerializableValue) => { 2611 | this.#server.sendLoggingMessage({ 2612 | level: "info", 2613 | data: { 2614 | message, 2615 | context, 2616 | }, 2617 | }); 2618 | }, 2619 | warn: (message: string, context?: SerializableValue) => { 2620 | this.#server.sendLoggingMessage({ 2621 | level: "warning", 2622 | data: { 2623 | message, 2624 | context, 2625 | }, 2626 | }); 2627 | }, 2628 | }; 2629 | 2630 | const maybeStringResult = await tool.execute(args, { 2631 | reportProgress, 2632 | log, 2633 | session: this.#auth, 2634 | }); 2635 | 2636 | if (typeof maybeStringResult === "string") { 2637 | result = ContentResultZodSchema.parse({ 2638 | content: [{ type: "text", text: maybeStringResult }], 2639 | }); 2640 | } else if ("type" in maybeStringResult) { 2641 | result = ContentResultZodSchema.parse({ 2642 | content: [maybeStringResult], 2643 | }); 2644 | } else { 2645 | result = ContentResultZodSchema.parse(maybeStringResult); 2646 | } 2647 | } catch (error) { 2648 | if (error instanceof UserError) { 2649 | return { 2650 | content: [{ type: "text", text: error.message }], 2651 | isError: true, 2652 | }; 2653 | } 2654 | 2655 | return { 2656 | content: [{ type: "text", text: `Error: ${error}` }], 2657 | isError: true, 2658 | }; 2659 | } 2660 | 2661 | return result; 2662 | }); 2663 | } 2664 | 2665 | private setupResourceHandlers(resources: Resource[]) { 2666 | this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { 2667 | return { 2668 | resources: resources.map((resource) => { 2669 | return { 2670 | uri: resource.uri, 2671 | name: resource.name, 2672 | mimeType: resource.mimeType, 2673 | }; 2674 | }), 2675 | }; 2676 | }); 2677 | 2678 | this.#server.setRequestHandler( 2679 | ReadResourceRequestSchema, 2680 | async (request) => { 2681 | if ("uri" in request.params) { 2682 | const resource = resources.find( 2683 | (resource) => 2684 | "uri" in resource && resource.uri === request.params.uri, 2685 | ); 2686 | 2687 | if (!resource) { 2688 | for (const resourceTemplate of this.#resourceTemplates) { 2689 | const uriTemplate = parseURITemplate( 2690 | resourceTemplate.uriTemplate, 2691 | ); 2692 | 2693 | const match = uriTemplate.fromUri(request.params.uri); 2694 | 2695 | if (!match) { 2696 | continue; 2697 | } 2698 | 2699 | const uri = uriTemplate.fill(match); 2700 | 2701 | const result = await resourceTemplate.load(match); 2702 | 2703 | return { 2704 | contents: [ 2705 | { 2706 | uri: uri, 2707 | mimeType: resourceTemplate.mimeType, 2708 | name: resourceTemplate.name, 2709 | ...result, 2710 | }, 2711 | ], 2712 | }; 2713 | } 2714 | 2715 | throw new McpError( 2716 | ErrorCode.MethodNotFound, 2717 | `Unknown resource: ${request.params.uri}`, 2718 | ); 2719 | } 2720 | 2721 | if (!("uri" in resource)) { 2722 | throw new UnexpectedStateError("Resource does not support reading"); 2723 | } 2724 | 2725 | let maybeArrayResult: Awaited<ReturnType<Resource["load"]>>; 2726 | 2727 | try { 2728 | maybeArrayResult = await resource.load(); 2729 | } catch (error) { 2730 | throw new McpError( 2731 | ErrorCode.InternalError, 2732 | `Error reading resource: ${error}`, 2733 | { 2734 | uri: resource.uri, 2735 | }, 2736 | ); 2737 | } 2738 | 2739 | if (Array.isArray(maybeArrayResult)) { 2740 | return { 2741 | contents: maybeArrayResult.map((result) => ({ 2742 | uri: resource.uri, 2743 | mimeType: resource.mimeType, 2744 | name: resource.name, 2745 | ...result, 2746 | })), 2747 | }; 2748 | } else { 2749 | return { 2750 | contents: [ 2751 | { 2752 | uri: resource.uri, 2753 | mimeType: resource.mimeType, 2754 | name: resource.name, 2755 | ...maybeArrayResult, 2756 | }, 2757 | ], 2758 | }; 2759 | } 2760 | } 2761 | 2762 | throw new UnexpectedStateError("Unknown resource request", { 2763 | request, 2764 | }); 2765 | }, 2766 | ); 2767 | } 2768 | 2769 | private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) { 2770 | this.#server.setRequestHandler( 2771 | ListResourceTemplatesRequestSchema, 2772 | async () => { 2773 | return { 2774 | resourceTemplates: resourceTemplates.map((resourceTemplate) => { 2775 | return { 2776 | name: resourceTemplate.name, 2777 | uriTemplate: resourceTemplate.uriTemplate, 2778 | }; 2779 | }), 2780 | }; 2781 | }, 2782 | ); 2783 | } 2784 | 2785 | private setupPromptHandlers(prompts: Prompt[]) { 2786 | this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { 2787 | return { 2788 | prompts: prompts.map((prompt) => { 2789 | return { 2790 | name: prompt.name, 2791 | description: prompt.description, 2792 | arguments: prompt.arguments, 2793 | complete: prompt.complete, 2794 | }; 2795 | }), 2796 | }; 2797 | }); 2798 | 2799 | this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { 2800 | const prompt = prompts.find( 2801 | (prompt) => prompt.name === request.params.name, 2802 | ); 2803 | 2804 | if (!prompt) { 2805 | throw new McpError( 2806 | ErrorCode.MethodNotFound, 2807 | `Unknown prompt: ${request.params.name}`, 2808 | ); 2809 | } 2810 | 2811 | const args = request.params.arguments; 2812 | 2813 | for (const arg of prompt.arguments ?? []) { 2814 | if (arg.required && !(args && arg.name in args)) { 2815 | throw new McpError( 2816 | ErrorCode.InvalidRequest, 2817 | `Missing required argument: ${arg.name}`, 2818 | ); 2819 | } 2820 | } 2821 | 2822 | let result: Awaited<ReturnType<Prompt["load"]>>; 2823 | 2824 | try { 2825 | result = await prompt.load(args as Record<string, string | undefined>); 2826 | } catch (error) { 2827 | throw new McpError( 2828 | ErrorCode.InternalError, 2829 | `Error loading prompt: ${error}`, 2830 | ); 2831 | } 2832 | 2833 | return { 2834 | description: prompt.description, 2835 | messages: [ 2836 | { 2837 | role: "user", 2838 | content: { type: "text", text: result }, 2839 | }, 2840 | ], 2841 | }; 2842 | }); 2843 | } 2844 | } 2845 | 2846 | const FastMCPEventEmitterBase: { 2847 | new (): StrictEventEmitter<EventEmitter, FastMCPEvents<FastMCPSessionAuth>>; 2848 | } = EventEmitter; 2849 | 2850 | class FastMCPEventEmitter extends FastMCPEventEmitterBase {} 2851 | 2852 | type Authenticate<T> = (request: http.IncomingMessage) => Promise<T>; 2853 | 2854 | export class FastMCP<T extends Record<string, unknown> | undefined = undefined> extends FastMCPEventEmitter { 2855 | #options: ServerOptions<T>; 2856 | #prompts: InputPrompt[] = []; 2857 | #resources: Resource[] = []; 2858 | #resourcesTemplates: InputResourceTemplate[] = []; 2859 | #sessions: FastMCPSession<T>[] = []; 2860 | #sseServer: SSEServer | null = null; 2861 | #tools: Tool<T>[] = []; 2862 | #authenticate: Authenticate<T> | undefined; 2863 | 2864 | constructor(public options: ServerOptions<T>) { 2865 | super(); 2866 | 2867 | this.#options = options; 2868 | this.#authenticate = options.authenticate; 2869 | } 2870 | 2871 | public get sessions(): FastMCPSession<T>[] { 2872 | return this.#sessions; 2873 | } 2874 | 2875 | /** 2876 | * Adds a tool to the server. 2877 | */ 2878 | public addTool<Params extends ToolParameters>(tool: Tool<T, Params>) { 2879 | this.#tools.push(tool as unknown as Tool<T>); 2880 | } 2881 | 2882 | /** 2883 | * Adds a resource to the server. 2884 | */ 2885 | public addResource(resource: Resource) { 2886 | this.#resources.push(resource); 2887 | } 2888 | 2889 | /** 2890 | * Adds a resource template to the server. 2891 | */ 2892 | public addResourceTemplate< 2893 | const Args extends InputResourceTemplateArgument[], 2894 | >(resource: InputResourceTemplate<Args>) { 2895 | this.#resourcesTemplates.push(resource); 2896 | } 2897 | 2898 | /** 2899 | * Adds a prompt to the server. 2900 | */ 2901 | public addPrompt<const Args extends InputPromptArgument[]>( 2902 | prompt: InputPrompt<Args>, 2903 | ) { 2904 | this.#prompts.push(prompt); 2905 | } 2906 | 2907 | /** 2908 | * Starts the server. 2909 | */ 2910 | public async start( 2911 | options: 2912 | | { transportType: "stdio" } 2913 | | { 2914 | transportType: "sse"; 2915 | sse: { endpoint: `/${string}`; port: number }; 2916 | } = { 2917 | transportType: "stdio", 2918 | }, 2919 | ) { 2920 | if (options.transportType === "stdio") { 2921 | const transport = new StdioServerTransport(); 2922 | 2923 | const session = new FastMCPSession<T>({ 2924 | name: this.#options.name, 2925 | version: this.#options.version, 2926 | tools: this.#tools, 2927 | resources: this.#resources, 2928 | resourcesTemplates: this.#resourcesTemplates, 2929 | prompts: this.#prompts, 2930 | }); 2931 | 2932 | await session.connect(transport); 2933 | 2934 | this.#sessions.push(session); 2935 | 2936 | this.emit("connect", { 2937 | session, 2938 | }); 2939 | 2940 | } else if (options.transportType === "sse") { 2941 | this.#sseServer = await startSSEServer<FastMCPSession<T>>({ 2942 | endpoint: options.sse.endpoint as `/${string}`, 2943 | port: options.sse.port, 2944 | createServer: async (request) => { 2945 | let auth: T | undefined; 2946 | 2947 | if (this.#authenticate) { 2948 | auth = await this.#authenticate(request); 2949 | } 2950 | 2951 | return new FastMCPSession<T>({ 2952 | auth, 2953 | name: this.#options.name, 2954 | version: this.#options.version, 2955 | tools: this.#tools, 2956 | resources: this.#resources, 2957 | resourcesTemplates: this.#resourcesTemplates, 2958 | prompts: this.#prompts, 2959 | }); 2960 | }, 2961 | onClose: (session) => { 2962 | this.emit("disconnect", { 2963 | session, 2964 | }); 2965 | }, 2966 | onConnect: async (session) => { 2967 | this.#sessions.push(session); 2968 | 2969 | this.emit("connect", { 2970 | session, 2971 | }); 2972 | }, 2973 | }); 2974 | 2975 | console.info( 2976 | `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`, 2977 | ); 2978 | } else { 2979 | throw new Error("Invalid transport type"); 2980 | } 2981 | } 2982 | 2983 | /** 2984 | * Stops the server. 2985 | */ 2986 | public async stop() { 2987 | if (this.#sseServer) { 2988 | this.#sseServer.close(); 2989 | } 2990 | } 2991 | } 2992 | 2993 | export type { Context }; 2994 | export type { Tool, ToolParameters }; 2995 | export type { Content, TextContent, ImageContent, ContentResult }; 2996 | export type { Progress, SerializableValue }; 2997 | export type { Resource, ResourceResult }; 2998 | export type { ResourceTemplate, ResourceTemplateArgument }; 2999 | export type { Prompt, PromptArgument }; 3000 | export type { InputPrompt, InputPromptArgument }; 3001 | export type { ServerOptions, LoggingLevel }; 3002 | export type { FastMCPEvents, FastMCPSessionEvents }; 3003 | 3004 | 3005 | 3006 | --- 3007 | File: /eslint.config.js 3008 | --- 3009 | 3010 | import perfectionist from "eslint-plugin-perfectionist"; 3011 | 3012 | export default [perfectionist.configs["recommended-alphabetical"]]; 3013 | 3014 | 3015 | 3016 | --- 3017 | File: /package.json 3018 | --- 3019 | 3020 | { 3021 | "name": "fastmcp", 3022 | "version": "1.0.0", 3023 | "main": "dist/FastMCP.js", 3024 | "scripts": { 3025 | "build": "tsup", 3026 | "test": "vitest run && tsc && jsr publish --dry-run", 3027 | "format": "prettier --write . && eslint --fix ." 3028 | }, 3029 | "bin": { 3030 | "fastmcp": "dist/bin/fastmcp.js" 3031 | }, 3032 | "keywords": [ 3033 | "MCP", 3034 | "SSE" 3035 | ], 3036 | "type": "module", 3037 | "author": "Frank Fiegel <[email protected]>", 3038 | "license": "MIT", 3039 | "description": "A TypeScript framework for building MCP servers.", 3040 | "module": "dist/FastMCP.js", 3041 | "types": "dist/FastMCP.d.ts", 3042 | "dependencies": { 3043 | "@modelcontextprotocol/sdk": "^1.6.0", 3044 | "execa": "^9.5.2", 3045 | "file-type": "^20.3.0", 3046 | "fuse.js": "^7.1.0", 3047 | "mcp-proxy": "^2.10.4", 3048 | "strict-event-emitter-types": "^2.0.0", 3049 | "undici": "^7.4.0", 3050 | "uri-templates": "^0.2.0", 3051 | "yargs": "^17.7.2", 3052 | "zod": "^3.24.2", 3053 | "zod-to-json-schema": "^3.24.3" 3054 | }, 3055 | "repository": { 3056 | "url": "https://github.com/punkpeye/fastmcp" 3057 | }, 3058 | "homepage": "https://glama.ai/mcp", 3059 | "release": { 3060 | "branches": [ 3061 | "main" 3062 | ], 3063 | "plugins": [ 3064 | "@semantic-release/commit-analyzer", 3065 | "@semantic-release/release-notes-generator", 3066 | "@semantic-release/npm", 3067 | "@semantic-release/github", 3068 | "@sebbo2002/semantic-release-jsr" 3069 | ] 3070 | }, 3071 | "devDependencies": { 3072 | "@sebbo2002/semantic-release-jsr": "^2.0.4", 3073 | "@tsconfig/node22": "^22.0.0", 3074 | "@types/node": "^22.13.5", 3075 | "@types/uri-templates": "^0.1.34", 3076 | "@types/yargs": "^17.0.33", 3077 | "eslint": "^9.21.0", 3078 | "eslint-plugin-perfectionist": "^4.9.0", 3079 | "eventsource-client": "^1.1.3", 3080 | "get-port-please": "^3.1.2", 3081 | "jsr": "^0.13.3", 3082 | "prettier": "^3.5.2", 3083 | "semantic-release": "^24.2.3", 3084 | "tsup": "^8.4.0", 3085 | "typescript": "^5.7.3", 3086 | "vitest": "^3.0.7" 3087 | }, 3088 | "tsup": { 3089 | "entry": [ 3090 | "src/FastMCP.ts", 3091 | "src/bin/fastmcp.ts" 3092 | ], 3093 | "format": [ 3094 | "esm" 3095 | ], 3096 | "dts": true, 3097 | "splitting": true, 3098 | "sourcemap": true, 3099 | "clean": true 3100 | } 3101 | } 3102 | 3103 | 3104 | 3105 | --- 3106 | File: /README.md 3107 | --- 3108 | 3109 | # FastMCP 3110 | 3111 | A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable of handling client sessions. 3112 | 3113 | > [!NOTE] 3114 | > 3115 | > For a Python implementation, see [FastMCP](https://github.com/jlowin/fastmcp). 3116 | 3117 | ## Features 3118 | 3119 | - Simple Tool, Resource, Prompt definition 3120 | - [Authentication](#authentication) 3121 | - [Sessions](#sessions) 3122 | - [Image content](#returning-an-image) 3123 | - [Logging](#logging) 3124 | - [Error handling](#errors) 3125 | - [SSE](#sse) 3126 | - CORS (enabled by default) 3127 | - [Progress notifications](#progress) 3128 | - [Typed server events](#typed-server-events) 3129 | - [Prompt argument auto-completion](#prompt-argument-auto-completion) 3130 | - [Sampling](#requestsampling) 3131 | - Automated SSE pings 3132 | - Roots 3133 | - CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector) 3134 | 3135 | ## Installation 3136 | 3137 | ```bash 3138 | npm install fastmcp 3139 | ``` 3140 | 3141 | ## Quickstart 3142 | 3143 | ```ts 3144 | import { FastMCP } from "fastmcp"; 3145 | import { z } from "zod"; 3146 | 3147 | const server = new FastMCP({ 3148 | name: "My Server", 3149 | version: "1.0.0", 3150 | }); 3151 | 3152 | server.addTool({ 3153 | name: "add", 3154 | description: "Add two numbers", 3155 | parameters: z.object({ 3156 | a: z.number(), 3157 | b: z.number(), 3158 | }), 3159 | execute: async (args) => { 3160 | return String(args.a + args.b); 3161 | }, 3162 | }); 3163 | 3164 | server.start({ 3165 | transportType: "stdio", 3166 | }); 3167 | ``` 3168 | 3169 | _That's it!_ You have a working MCP server. 3170 | 3171 | You can test the server in terminal with: 3172 | 3173 | ```bash 3174 | git clone https://github.com/punkpeye/fastmcp.git 3175 | cd fastmcp 3176 | 3177 | npm install 3178 | 3179 | # Test the addition server example using CLI: 3180 | npx fastmcp dev src/examples/addition.ts 3181 | # Test the addition server example using MCP Inspector: 3182 | npx fastmcp inspect src/examples/addition.ts 3183 | ``` 3184 | 3185 | ### SSE 3186 | 3187 | You can also run the server with SSE support: 3188 | 3189 | ```ts 3190 | server.start({ 3191 | transportType: "sse", 3192 | sse: { 3193 | endpoint: "/sse", 3194 | port: 8080, 3195 | }, 3196 | }); 3197 | ``` 3198 | 3199 | This will start the server and listen for SSE connections on `http://localhost:8080/sse`. 3200 | 3201 | You can then use `SSEClientTransport` to connect to the server: 3202 | 3203 | ```ts 3204 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; 3205 | 3206 | const client = new Client( 3207 | { 3208 | name: "example-client", 3209 | version: "1.0.0", 3210 | }, 3211 | { 3212 | capabilities: {}, 3213 | }, 3214 | ); 3215 | 3216 | const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)); 3217 | 3218 | await client.connect(transport); 3219 | ``` 3220 | 3221 | ## Core Concepts 3222 | 3223 | ### Tools 3224 | 3225 | [Tools](https://modelcontextprotocol.io/docs/concepts/tools) in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. 3226 | 3227 | ```js 3228 | server.addTool({ 3229 | name: "fetch", 3230 | description: "Fetch the content of a url", 3231 | parameters: z.object({ 3232 | url: z.string(), 3233 | }), 3234 | execute: async (args) => { 3235 | return await fetchWebpageContent(args.url); 3236 | }, 3237 | }); 3238 | ``` 3239 | 3240 | #### Returning a string 3241 | 3242 | `execute` can return a string: 3243 | 3244 | ```js 3245 | server.addTool({ 3246 | name: "download", 3247 | description: "Download a file", 3248 | parameters: z.object({ 3249 | url: z.string(), 3250 | }), 3251 | execute: async (args) => { 3252 | return "Hello, world!"; 3253 | }, 3254 | }); 3255 | ``` 3256 | 3257 | The latter is equivalent to: 3258 | 3259 | ```js 3260 | server.addTool({ 3261 | name: "download", 3262 | description: "Download a file", 3263 | parameters: z.object({ 3264 | url: z.string(), 3265 | }), 3266 | execute: async (args) => { 3267 | return { 3268 | content: [ 3269 | { 3270 | type: "text", 3271 | text: "Hello, world!", 3272 | }, 3273 | ], 3274 | }; 3275 | }, 3276 | }); 3277 | ``` 3278 | 3279 | #### Returning a list 3280 | 3281 | If you want to return a list of messages, you can return an object with a `content` property: 3282 | 3283 | ```js 3284 | server.addTool({ 3285 | name: "download", 3286 | description: "Download a file", 3287 | parameters: z.object({ 3288 | url: z.string(), 3289 | }), 3290 | execute: async (args) => { 3291 | return { 3292 | content: [ 3293 | { type: "text", text: "First message" }, 3294 | { type: "text", text: "Second message" }, 3295 | ], 3296 | }; 3297 | }, 3298 | }); 3299 | ``` 3300 | 3301 | #### Returning an image 3302 | 3303 | Use the `imageContent` to create a content object for an image: 3304 | 3305 | ```js 3306 | import { imageContent } from "fastmcp"; 3307 | 3308 | server.addTool({ 3309 | name: "download", 3310 | description: "Download a file", 3311 | parameters: z.object({ 3312 | url: z.string(), 3313 | }), 3314 | execute: async (args) => { 3315 | return imageContent({ 3316 | url: "https://example.com/image.png", 3317 | }); 3318 | 3319 | // or... 3320 | // return imageContent({ 3321 | // path: "/path/to/image.png", 3322 | // }); 3323 | 3324 | // or... 3325 | // return imageContent({ 3326 | // buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"), 3327 | // }); 3328 | 3329 | // or... 3330 | // return { 3331 | // content: [ 3332 | // await imageContent(...) 3333 | // ], 3334 | // }; 3335 | }, 3336 | }); 3337 | ``` 3338 | 3339 | The `imageContent` function takes the following options: 3340 | 3341 | - `url`: The URL of the image. 3342 | - `path`: The path to the image file. 3343 | - `buffer`: The image data as a buffer. 3344 | 3345 | Only one of `url`, `path`, or `buffer` must be specified. 3346 | 3347 | The above example is equivalent to: 3348 | 3349 | ```js 3350 | server.addTool({ 3351 | name: "download", 3352 | description: "Download a file", 3353 | parameters: z.object({ 3354 | url: z.string(), 3355 | }), 3356 | execute: async (args) => { 3357 | return { 3358 | content: [ 3359 | { 3360 | type: "image", 3361 | data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", 3362 | mimeType: "image/png", 3363 | }, 3364 | ], 3365 | }; 3366 | }, 3367 | }); 3368 | ``` 3369 | 3370 | #### Logging 3371 | 3372 | Tools can log messages to the client using the `log` object in the context object: 3373 | 3374 | ```js 3375 | server.addTool({ 3376 | name: "download", 3377 | description: "Download a file", 3378 | parameters: z.object({ 3379 | url: z.string(), 3380 | }), 3381 | execute: async (args, { log }) => { 3382 | log.info("Downloading file...", { 3383 | url, 3384 | }); 3385 | 3386 | // ... 3387 | 3388 | log.info("Downloaded file"); 3389 | 3390 | return "done"; 3391 | }, 3392 | }); 3393 | ``` 3394 | 3395 | The `log` object has the following methods: 3396 | 3397 | - `debug(message: string, data?: SerializableValue)` 3398 | - `error(message: string, data?: SerializableValue)` 3399 | - `info(message: string, data?: SerializableValue)` 3400 | - `warn(message: string, data?: SerializableValue)` 3401 | 3402 | #### Errors 3403 | 3404 | The errors that are meant to be shown to the user should be thrown as `UserError` instances: 3405 | 3406 | ```js 3407 | import { UserError } from "fastmcp"; 3408 | 3409 | server.addTool({ 3410 | name: "download", 3411 | description: "Download a file", 3412 | parameters: z.object({ 3413 | url: z.string(), 3414 | }), 3415 | execute: async (args) => { 3416 | if (args.url.startsWith("https://example.com")) { 3417 | throw new UserError("This URL is not allowed"); 3418 | } 3419 | 3420 | return "done"; 3421 | }, 3422 | }); 3423 | ``` 3424 | 3425 | #### Progress 3426 | 3427 | Tools can report progress by calling `reportProgress` in the context object: 3428 | 3429 | ```js 3430 | server.addTool({ 3431 | name: "download", 3432 | description: "Download a file", 3433 | parameters: z.object({ 3434 | url: z.string(), 3435 | }), 3436 | execute: async (args, { reportProgress }) => { 3437 | reportProgress({ 3438 | progress: 0, 3439 | total: 100, 3440 | }); 3441 | 3442 | // ... 3443 | 3444 | reportProgress({ 3445 | progress: 100, 3446 | total: 100, 3447 | }); 3448 | 3449 | return "done"; 3450 | }, 3451 | }); 3452 | ``` 3453 | 3454 | ### Resources 3455 | 3456 | [Resources](https://modelcontextprotocol.io/docs/concepts/resources) represent any kind of data that an MCP server wants to make available to clients. This can include: 3457 | 3458 | - File contents 3459 | - Screenshots and images 3460 | - Log files 3461 | - And more 3462 | 3463 | Each resource is identified by a unique URI and can contain either text or binary data. 3464 | 3465 | ```ts 3466 | server.addResource({ 3467 | uri: "file:///logs/app.log", 3468 | name: "Application Logs", 3469 | mimeType: "text/plain", 3470 | async load() { 3471 | return { 3472 | text: await readLogFile(), 3473 | }; 3474 | }, 3475 | }); 3476 | ``` 3477 | 3478 | > [!NOTE] 3479 | > 3480 | > `load` can return multiple resources. This could be used, for example, to return a list of files inside a directory when the directory is read. 3481 | > 3482 | > ```ts 3483 | > async load() { 3484 | > return [ 3485 | > { 3486 | > text: "First file content", 3487 | > }, 3488 | > { 3489 | > text: "Second file content", 3490 | > }, 3491 | > ]; 3492 | > } 3493 | > ``` 3494 | 3495 | You can also return binary contents in `load`: 3496 | 3497 | ```ts 3498 | async load() { 3499 | return { 3500 | blob: 'base64-encoded-data' 3501 | }; 3502 | } 3503 | ``` 3504 | 3505 | ### Resource templates 3506 | 3507 | You can also define resource templates: 3508 | 3509 | ```ts 3510 | server.addResourceTemplate({ 3511 | uriTemplate: "file:///logs/{name}.log", 3512 | name: "Application Logs", 3513 | mimeType: "text/plain", 3514 | arguments: [ 3515 | { 3516 | name: "name", 3517 | description: "Name of the log", 3518 | required: true, 3519 | }, 3520 | ], 3521 | async load({ name }) { 3522 | return { 3523 | text: `Example log content for ${name}`, 3524 | }; 3525 | }, 3526 | }); 3527 | ``` 3528 | 3529 | #### Resource template argument auto-completion 3530 | 3531 | Provide `complete` functions for resource template arguments to enable automatic completion: 3532 | 3533 | ```ts 3534 | server.addResourceTemplate({ 3535 | uriTemplate: "file:///logs/{name}.log", 3536 | name: "Application Logs", 3537 | mimeType: "text/plain", 3538 | arguments: [ 3539 | { 3540 | name: "name", 3541 | description: "Name of the log", 3542 | required: true, 3543 | complete: async (value) => { 3544 | if (value === "Example") { 3545 | return { 3546 | values: ["Example Log"], 3547 | }; 3548 | } 3549 | 3550 | return { 3551 | values: [], 3552 | }; 3553 | }, 3554 | }, 3555 | ], 3556 | async load({ name }) { 3557 | return { 3558 | text: `Example log content for ${name}`, 3559 | }; 3560 | }, 3561 | }); 3562 | ``` 3563 | 3564 | ### Prompts 3565 | 3566 | [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts) enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions. 3567 | 3568 | ```ts 3569 | server.addPrompt({ 3570 | name: "git-commit", 3571 | description: "Generate a Git commit message", 3572 | arguments: [ 3573 | { 3574 | name: "changes", 3575 | description: "Git diff or description of changes", 3576 | required: true, 3577 | }, 3578 | ], 3579 | load: async (args) => { 3580 | return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; 3581 | }, 3582 | }); 3583 | ``` 3584 | 3585 | #### Prompt argument auto-completion 3586 | 3587 | Prompts can provide auto-completion for their arguments: 3588 | 3589 | ```js 3590 | server.addPrompt({ 3591 | name: "countryPoem", 3592 | description: "Writes a poem about a country", 3593 | load: async ({ name }) => { 3594 | return `Hello, ${name}!`; 3595 | }, 3596 | arguments: [ 3597 | { 3598 | name: "name", 3599 | description: "Name of the country", 3600 | required: true, 3601 | complete: async (value) => { 3602 | if (value === "Germ") { 3603 | return { 3604 | values: ["Germany"], 3605 | }; 3606 | } 3607 | 3608 | return { 3609 | values: [], 3610 | }; 3611 | }, 3612 | }, 3613 | ], 3614 | }); 3615 | ``` 3616 | 3617 | #### Prompt argument auto-completion using `enum` 3618 | 3619 | If you provide an `enum` array for an argument, the server will automatically provide completions for the argument. 3620 | 3621 | ```js 3622 | server.addPrompt({ 3623 | name: "countryPoem", 3624 | description: "Writes a poem about a country", 3625 | load: async ({ name }) => { 3626 | return `Hello, ${name}!`; 3627 | }, 3628 | arguments: [ 3629 | { 3630 | name: "name", 3631 | description: "Name of the country", 3632 | required: true, 3633 | enum: ["Germany", "France", "Italy"], 3634 | }, 3635 | ], 3636 | }); 3637 | ``` 3638 | 3639 | ### Authentication 3640 | 3641 | FastMCP allows you to `authenticate` clients using a custom function: 3642 | 3643 | ```ts 3644 | import { AuthError } from "fastmcp"; 3645 | 3646 | const server = new FastMCP({ 3647 | name: "My Server", 3648 | version: "1.0.0", 3649 | authenticate: ({request}) => { 3650 | const apiKey = request.headers["x-api-key"]; 3651 | 3652 | if (apiKey !== '123') { 3653 | throw new Response(null, { 3654 | status: 401, 3655 | statusText: "Unauthorized", 3656 | }); 3657 | } 3658 | 3659 | // Whatever you return here will be accessible in the `context.session` object. 3660 | return { 3661 | id: 1, 3662 | } 3663 | }, 3664 | }); 3665 | ``` 3666 | 3667 | Now you can access the authenticated session data in your tools: 3668 | 3669 | ```ts 3670 | server.addTool({ 3671 | name: "sayHello", 3672 | execute: async (args, { session }) => { 3673 | return `Hello, ${session.id}!`; 3674 | }, 3675 | }); 3676 | ``` 3677 | 3678 | ### Sessions 3679 | 3680 | The `session` object is an instance of `FastMCPSession` and it describes active client sessions. 3681 | 3682 | ```ts 3683 | server.sessions; 3684 | ``` 3685 | 3686 | We allocate a new server instance for each client connection to enable 1:1 communication between a client and the server. 3687 | 3688 | ### Typed server events 3689 | 3690 | You can listen to events emitted by the server using the `on` method: 3691 | 3692 | ```ts 3693 | server.on("connect", (event) => { 3694 | console.log("Client connected:", event.session); 3695 | }); 3696 | 3697 | server.on("disconnect", (event) => { 3698 | console.log("Client disconnected:", event.session); 3699 | }); 3700 | ``` 3701 | 3702 | ## `FastMCPSession` 3703 | 3704 | `FastMCPSession` represents a client session and provides methods to interact with the client. 3705 | 3706 | Refer to [Sessions](#sessions) for examples of how to obtain a `FastMCPSession` instance. 3707 | 3708 | ### `requestSampling` 3709 | 3710 | `requestSampling` creates a [sampling](https://modelcontextprotocol.io/docs/concepts/sampling) request and returns the response. 3711 | 3712 | ```ts 3713 | await session.requestSampling({ 3714 | messages: [ 3715 | { 3716 | role: "user", 3717 | content: { 3718 | type: "text", 3719 | text: "What files are in the current directory?", 3720 | }, 3721 | }, 3722 | ], 3723 | systemPrompt: "You are a helpful file system assistant.", 3724 | includeContext: "thisServer", 3725 | maxTokens: 100, 3726 | }); 3727 | ``` 3728 | 3729 | ### `clientCapabilities` 3730 | 3731 | The `clientCapabilities` property contains the client capabilities. 3732 | 3733 | ```ts 3734 | session.clientCapabilities; 3735 | ``` 3736 | 3737 | ### `loggingLevel` 3738 | 3739 | The `loggingLevel` property describes the logging level as set by the client. 3740 | 3741 | ```ts 3742 | session.loggingLevel; 3743 | ``` 3744 | 3745 | ### `roots` 3746 | 3747 | The `roots` property contains the roots as set by the client. 3748 | 3749 | ```ts 3750 | session.roots; 3751 | ``` 3752 | 3753 | ### `server` 3754 | 3755 | The `server` property contains an instance of MCP server that is associated with the session. 3756 | 3757 | ```ts 3758 | session.server; 3759 | ``` 3760 | 3761 | ### Typed session events 3762 | 3763 | You can listen to events emitted by the session using the `on` method: 3764 | 3765 | ```ts 3766 | session.on("rootsChanged", (event) => { 3767 | console.log("Roots changed:", event.roots); 3768 | }); 3769 | 3770 | session.on("error", (event) => { 3771 | console.error("Error:", event.error); 3772 | }); 3773 | ``` 3774 | 3775 | ## Running Your Server 3776 | 3777 | ### Test with `mcp-cli` 3778 | 3779 | The fastest way to test and debug your server is with `fastmcp dev`: 3780 | 3781 | ```bash 3782 | npx fastmcp dev server.js 3783 | npx fastmcp dev server.ts 3784 | ``` 3785 | 3786 | This will run your server with [`mcp-cli`](https://github.com/wong2/mcp-cli) for testing and debugging your MCP server in the terminal. 3787 | 3788 | ### Inspect with `MCP Inspector` 3789 | 3790 | Another way is to use the official [`MCP Inspector`](https://modelcontextprotocol.io/docs/tools/inspector) to inspect your server with a Web UI: 3791 | 3792 | ```bash 3793 | npx fastmcp inspect server.ts 3794 | ``` 3795 | 3796 | ## FAQ 3797 | 3798 | ### How to use with Claude Desktop? 3799 | 3800 | Follow the guide https://modelcontextprotocol.io/quickstart/user and add the following configuration: 3801 | 3802 | ```json 3803 | { 3804 | "mcpServers": { 3805 | "my-mcp-server": { 3806 | "command": "npx", 3807 | "args": [ 3808 | "tsx", 3809 | "/PATH/TO/YOUR_PROJECT/src/index.ts" 3810 | ], 3811 | "env": { 3812 | "YOUR_ENV_VAR": "value" 3813 | } 3814 | } 3815 | } 3816 | } 3817 | ``` 3818 | 3819 | ## Showcase 3820 | 3821 | > [!NOTE] 3822 | > 3823 | > If you've developed a server using FastMCP, please [submit a PR](https://github.com/punkpeye/fastmcp) to showcase it here! 3824 | 3825 | - https://github.com/apinetwork/piapi-mcp-server 3826 | - https://github.com/Meeting-Baas/meeting-mcp - Meeting BaaS MCP server that enables AI assistants to create meeting bots, search transcripts, and manage recording data 3827 | 3828 | ## Acknowledgements 3829 | 3830 | - FastMCP is inspired by the [Python implementation](https://github.com/jlowin/fastmcp) by [Jonathan Lowin](https://github.com/jlowin). 3831 | - Parts of codebase were adopted from [LiteMCP](https://github.com/wong2/litemcp). 3832 | - Parts of codebase were adopted from [Model Context protocolでSSEをやってみる](https://dev.classmethod.jp/articles/mcp-sse/). 3833 | 3834 | 3835 | 3836 | --- 3837 | File: /vitest.config.js 3838 | --- 3839 | 3840 | import { defineConfig } from "vitest/config"; 3841 | 3842 | export default defineConfig({ 3843 | test: { 3844 | poolOptions: { 3845 | forks: { execArgv: ["--experimental-eventsource"] }, 3846 | }, 3847 | }, 3848 | }); 3849 | 3850 | ```