This is page 65 of 69. 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
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .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
│ │ └── validate-changesets.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
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── 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
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.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_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── 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
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── 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
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── 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
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── 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_CODE_PLUGIN.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
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── 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
│ │ │ ├── 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
│ │ │ ├── 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
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── 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
│ ├── tool-registry.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
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.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
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.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
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.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
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.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
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.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
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── 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
│ │ └── providers
│ │ └── temperature-support.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
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.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
│ │ └── tool-registration.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
│ │ └── prompt-migration.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
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.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
└── update-task-migration-plan.md
```
# 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 |
```