This is page 34 of 38. Use http://codebase.md/eyaltoledano/claude-task-master?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /context/chats/add-task-dependencies-1.md: -------------------------------------------------------------------------------- ```markdown # Task Title Formatting Issue Discussion _Exported on 5/22/2025 at 00:05:09 EDT from Cursor (0.50.5)_ --- **User** in add-task, i noticed that with some providers, the title comes out with the id in it. i dont know if this is a parsing issue (@ollama.js) or if it this is an improvement we ought to make to the system prompt in @add-task.js --- **Cursor** Okay, I've looked into this. The issue where the task ID is sometimes included in the generated task title (e.g., "Task #89: Introduce Prioritize Command...") likely stems from the prompt construction in `scripts/modules/task-manager/add-task.js`. The current user prompt (around line 208) is: `Create a comprehensive new task (Task #${newTaskId}) for a software development project based on this description: "${prompt}"` The AI might be interpreting `(Task #${newTaskId})` as part of the content it needs to generate, especially for the title. The `ollama.js` provider itself doesn't seem to be modifying the content in a way that would add the ID; it primarily passes the prompt and schema to the Ollama service. I'll adjust the user prompt in `scripts/modules/task-manager/add-task.js` to clarify that the task ID is contextual information, not part of the title to be generated. I'll change this part of the prompt: `Create a comprehensive new task (Task #${newTaskId}) for a software development project based on this description: "${prompt}"` to something like: `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.` This rephrasing should guide the AI to use the ID as context rather than including it directly in the `title` field of the generated JSON object. Here's the proposed change: ```javascript:scripts/modules/task-manager/add-task.js // ... existing code ... if (manualTaskData?.testStrategy) contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific.`; // Start the loading indicator - only for text mode if (outputFormat === 'text') { // ... existing code ... ``` This change clarifies the role of `newTaskId` for the AI. Let me know if you'd like to test this or make further adjustments! ```javascript // ... existing code ... if (manualTaskData?.testStrategy) contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific.`; // Start the loading indicator - only for text mode if (outputFormat === 'text') { // ... existing code ... ``` --- **User** get the details of task id 88 -- i wonder if we can - in the above place, be smart about passing in the ids, titles, descriptions (but not details) and dependencies of all the other tasks i don't want it to take a ton of context, but the goal here is to also fill in the dependency field when creating a task. right now it's not being filled in and i have to update all the tasks for it to be populated. so i want it to be fixed at the source in add-task ideally and i want a lightweight but accurate way to pass in the other tasks and their dependencies, and not passing in the actual details of the task and only the above as i explained might be enough. --- **Cursor** Let me get the details of task ID 88 first: Now I'll look at the current implementation of add-task.js to understand how we can improve the context provided to the AI: Based on the task details and the current implementation of add-task.js, I'll create an improved version that provides more context about other tasks and their dependency relationships without overwhelming the token limit. Here's my proposed change: ```javascript // ... existing code ... report('DEBUG: Taking AI task generation path.', 'debug'); // --- Refactored AI Interaction --- report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); // Create context string for task creation prompt let contextTasks = ''; // Create a dependency map for better understanding of the task relationships const taskMap = {}; data.tasks.forEach((t) => { // For each task, only include id, title, description, and dependencies taskMap[t.id] = { id: t.id, title: t.title, description: t.description, dependencies: t.dependencies || [], status: t.status }; }); if (numericDependencies.length > 0) { // If specific dependencies were provided, focus on them const dependentTasks = data.tasks.filter((t) => numericDependencies.includes(t.id) ); contextTasks = `\nThis task depends on the following tasks:\n${dependentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; // Add dependency chain information (what these tasks depend on) const dependencyChains = []; for (const depTask of dependentTasks) { if (depTask.dependencies && depTask.dependencies.length > 0) { const subDeps = depTask.dependencies.map((subDepId) => { const subDepTask = data.tasks.find((t) => t.id === subDepId); return subDepTask ? `Task ${subDepId}: ${subDepTask.title}` : `Task ${subDepId}`; }); dependencyChains.push( `Task ${depTask.id} depends on: ${subDeps.join(', ')}` ); } } if (dependencyChains.length > 0) { contextTasks += `\n\nDependency chains:\n${dependencyChains.join('\n')}`; } } else { // If no dependencies provided, show recent and related tasks // Get recent tasks (newest first) const recentTasks = [...data.tasks].sort((a, b) => b.id - a.id).slice(0, 5); // Get tasks that might be related based on simple keyword matching const keywords = prompt .toLowerCase() .split(/\s+/) .filter( (word) => word.length > 4 && !['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word) ); const relatedTasks = keywords.length > 0 ? data.tasks .filter( (t) => keywords.some( (keyword) => t.title.toLowerCase().includes(keyword) || t.description.toLowerCase().includes(keyword) ) && !recentTasks.some((rt) => rt.id === t.id) ) .slice(0, 3) : []; if (recentTasks.length > 0) { contextTasks = `\nRecent tasks in the project:\n${recentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } if (relatedTasks.length > 0) { contextTasks += `\n\nPotentially related tasks based on keywords:\n${relatedTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } // Add a concise view of the task dependency structure contextTasks += '\n\nSummary of task dependencies in the project:'; const pendingTasks = data.tasks.filter((t) => t.status !== 'done').slice(-10); for (const task of pendingTasks) { const depsStr = task.dependencies && task.dependencies.length > 0 ? task.dependencies.join(', ') : 'None'; contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; } } // System Prompt - Enhanced for dependency awareness const systemPrompt = "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on."; // Task Structure Description (for user prompt) const taskStructureDesc = ` { "title": "Task title goes here", "description": "A concise one or two sentence description of what the task involves", "details": "In-depth implementation details, considerations, and guidance.", "testStrategy": "Detailed approach for verifying task completion." }`; // Add any manually provided details to the prompt for context let contextFromArgs = ''; if (manualTaskData?.title) contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; if (manualTaskData?.description) contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; if (manualTaskData?.details) contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; if (manualTaskData?.testStrategy) contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the information about existing tasks and their dependencies, suggest appropriate dependencies for this task if any are needed. The task IDs mentioned above can be referenced in your implementation details. Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific. Don't include the task ID in the title.`; // ... existing code ... ``` Now let's test the changes to see if they improve the task creation process, especially around dependency handling: --- **User** this will probably need to be adjusted so from a structured output pov the ai knows to add the info in that field --- **Cursor** You're absolutely right. While our changes improve the context provided to the AI, they don't provide a structured way for the AI to include the dependencies in its output schema. Let's modify the Zod schema to explicitly include a `dependencies` field: ```javascript // Define Zod schema for the expected AI output object const AiTaskDataSchema = z.object({ title: z.string().describe('Clear, concise title for the task'), description: z .string() .describe('A one or two sentence description of the task'), details: z .string() .describe('In-depth implementation details, considerations, and guidance'), testStrategy: z .string() .describe('Detailed approach for verifying task completion'), dependencies: z .array(z.number()) .optional() .describe( 'Array of task IDs that this task depends on (must be completed before this task can start)' ) }); // ... existing code ... // Task Structure Description (for user prompt) const taskStructureDesc = ` { "title": "Task title goes here", "description": "A concise one or two sentence description of what the task involves", "details": "In-depth implementation details, considerations, and guidance.", "testStrategy": "Detailed approach for verifying task completion.", "dependencies": [1, 2, 3] // Optional: Array of existing task IDs that this task depends on }`; // ... existing code ... // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the information about existing tasks and their dependencies, include appropriate dependencies in the "dependencies" array if any are needed. Only include task IDs that this new task directly depends on. Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific. Don't include the task ID in the title.`; // ... existing code ... // Create the new task object const newTask = { id: newTaskId, title: taskData.title, description: taskData.description, details: taskData.details || '', testStrategy: taskData.testStrategy || '', status: 'pending', dependencies: taskData.dependencies?.length ? taskData.dependencies : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified priority: effectivePriority, subtasks: [] // Initialize with empty subtasks array }; ``` Let's make one more small fix - I notice there's still a reference to `numericDependencies` in the success message output that should be adjusted to use the task's actual dependencies: ```javascript // ... existing code ... console.log( boxen( chalk.white.bold(`Task ${newTaskId} Created Successfully`) + '\n\n' + chalk.white(`Title: ${newTask.title}`) + '\n' + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + '\n' + chalk.white( `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` ) + '\n' + (newTask.dependencies.length > 0 ? chalk.white(`Dependencies: ${newTask.dependencies.join(', ')}`) + '\n' : '') + '\n' + // ... existing code ... ``` Now let's test our changes to make sure they work properly: --- **User** looks like it worked. i would like -- in the case of the CLI only, to also show some additional stuff in @add-task.js we just did a bunch of magic, and it would be too bad not to show it to the user as we do it, without it being too much (ignore the failed ollama call) i think there could be a line or two that shows the identified dependencies or something. its sexy stuff, let's make sure the user knows about it elegantly. @ui.js --- **Cursor** Let's enhance the CLI experience to elegantly showcase the dependency analysis we're performing. I'll add a small, informative display in add-task.js that shows the intelligent context we're building: ```javascript // ... existing code ... // Create a dependency map for better understanding of the task relationships const taskMap = {}; data.tasks.forEach((t) => { // For each task, only include id, title, description, and dependencies taskMap[t.id] = { id: t.id, title: t.title, description: t.description, dependencies: t.dependencies || [], status: t.status }; }); // CLI-only feedback for the dependency analysis if (outputFormat === 'text') { console.log( boxen(chalk.cyan.bold('Task Context Analysis') + '\n', { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 0 }, borderColor: 'cyan', borderStyle: 'round' }) ); } if (numericDependencies.length > 0) { // If specific dependencies were provided, focus on them const dependentTasks = data.tasks.filter((t) => numericDependencies.includes(t.id) ); contextTasks = `\nThis task depends on the following tasks:\n${dependentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; // Add dependency chain information (what these tasks depend on) const dependencyChains = []; for (const depTask of dependentTasks) { if (depTask.dependencies && depTask.dependencies.length > 0) { const subDeps = depTask.dependencies.map((subDepId) => { const subDepTask = data.tasks.find((t) => t.id === subDepId); return subDepTask ? `Task ${subDepId}: ${subDepTask.title}` : `Task ${subDepId}`; }); dependencyChains.push( `Task ${depTask.id} depends on: ${subDeps.join(', ')}` ); } } if (dependencyChains.length > 0) { contextTasks += `\n\nDependency chains:\n${dependencyChains.join('\n')}`; } // Show dependency chains in CLI mode if (outputFormat === 'text' && dependentTasks.length > 0) { console.log(chalk.gray(` Explicitly specified dependencies:`)); dependentTasks.forEach((t) => { console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); if (dependencyChains.length > 0) { console.log(chalk.gray(`\n Analyzing dependency chains:`)); dependencyChains.forEach((chain) => { console.log(chalk.cyan(` • ${truncate(chain, 60)}`)); }); } console.log(); // Add spacing } } else { // If no dependencies provided, show recent and related tasks // Get recent tasks (newest first) const recentTasks = [...data.tasks].sort((a, b) => b.id - a.id).slice(0, 5); // Get tasks that might be related based on simple keyword matching const keywords = prompt .toLowerCase() .split(/\s+/) .filter( (word) => word.length > 4 && !['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word) ); const relatedTasks = keywords.length > 0 ? data.tasks .filter( (t) => keywords.some( (keyword) => t.title.toLowerCase().includes(keyword) || t.description.toLowerCase().includes(keyword) ) && !recentTasks.some((rt) => rt.id === t.id) ) .slice(0, 3) : []; if (recentTasks.length > 0) { contextTasks = `\nRecent tasks in the project:\n${recentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } if (relatedTasks.length > 0) { contextTasks += `\n\nPotentially related tasks based on keywords:\n${relatedTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } // Add a concise view of the task dependency structure contextTasks += '\n\nSummary of task dependencies in the project:'; const pendingTasks = data.tasks.filter((t) => t.status !== 'done').slice(-10); for (const task of pendingTasks) { const depsStr = task.dependencies && task.dependencies.length > 0 ? task.dependencies.join(', ') : 'None'; contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; } // Show keyword-based analysis in CLI mode if (outputFormat === 'text') { if (keywords.length > 0) { console.log( chalk.gray(` Keywords extracted for analysis: ${keywords.join(', ')}`) ); if (relatedTasks.length > 0) { console.log( chalk.gray(`\n Potentially related tasks by keyword match:`) ); relatedTasks.forEach((t) => { console.log( chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`) ); }); } } // Show the recent tasks used for context if (recentTasks.length > 0) { console.log(chalk.gray(`\n Using recent tasks for context:`)); recentTasks.slice(0, 3).forEach((t) => { console.log(chalk.cyan(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); } // Show dependency structure visualization if (pendingTasks.length > 0) { const depsToShow = Math.min(3, pendingTasks.length); console.log( chalk.gray( `\n Task dependency patterns analyzed (${depsToShow} example${depsToShow > 1 ? 's' : ''}):` ) ); pendingTasks.slice(0, depsToShow).forEach((task) => { const depsStr = task.dependencies && task.dependencies.length > 0 ? chalk.yellow(task.dependencies.join(', ')) : chalk.gray('None'); console.log(chalk.blue(` • Task ${task.id} → [${depsStr}]`)); }); } console.log(); // Add spacing } } // System Prompt - Enhanced for dependency awareness // ... existing code ... ``` Now let's also add a visual indicator before the AI call to make the transition clearer and make the experience more engaging: ```javascript // ... existing code ... console.log(); // Add spacing } } // System Prompt - Enhanced for dependency awareness const systemPrompt = "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on."; // Task Structure Description (for user prompt) const taskStructureDesc = ` { "title": "Task title goes here", "description": "A concise one or two sentence description of what the task involves", "details": "In-depth implementation details, considerations, and guidance.", "testStrategy": "Detailed approach for verifying task completion.", "dependencies": [1, 2, 3] // Optional: Array of existing task IDs that this task depends on }`; // Add any manually provided details to the prompt for context let contextFromArgs = ''; if (manualTaskData?.title) contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; if (manualTaskData?.description) contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; if (manualTaskData?.details) contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; if (manualTaskData?.testStrategy) contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the information about existing tasks and their dependencies, include appropriate dependencies in the "dependencies" array if any are needed. Only include task IDs that this new task directly depends on. Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific. Don't include the task ID in the title.`; // Start the loading indicator - only for text mode if (outputFormat === 'text') { // Add a visual transition to show we're moving to AI generation console.log(boxen( chalk.white.bold('AI Task Generation') + `\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + `\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + `\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}`, { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 0, bottom: 1 } } )); loadingIndicator = startLoadingIndicator( `Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n` ); } try { // ... existing code ... ``` Finally, let's enhance the success message to highlight the AI's dependency analysis and any dependencies added by the AI: ```javascript // ... existing code ... truncate(newTask.description, 47) ]); console.log(chalk.green('✅ New task created successfully:')); console.log(table.toString()); // Helper to get priority color const getPriorityColor = (p) => { switch (p?.toLowerCase()) { case 'high': return 'red'; case 'low': return 'gray'; case 'medium': default: return 'yellow'; } }; // Check if AI added new dependencies that weren't explicitly provided const aiAddedDeps = newTask.dependencies.filter( dep => !numericDependencies.includes(dep) ); // Show success message box console.log( boxen( chalk.white.bold(`Task ${newTaskId} Created Successfully`) + '\n\n' + chalk.white(`Title: ${newTask.title}`) + '\n' + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + '\n' + chalk.white( `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` ) + '\n' + (newTask.dependencies.length > 0 ? chalk.white(`Dependencies: ${newTask.dependencies.join(', ')}`) + (aiAddedDeps.length > 0 ? chalk.gray(` (${aiAddedDeps.length} auto-detected by AI)`) : '') + '\n' : '') + '\n' + chalk.white.bold('Next Steps:') + '\n' + // ... existing code ... ``` Let's test the changes to see our enhanced CLI experience in action: --- **User** it seems to have worked, but its now the second task that used 1 and 3 as dependencies. across 90 tasks, i find that a bit odd (but not impossible as most tasks are in fact net new) let's, for the sake of research, also include the details of the other tasks. let's see what happens with the context size --- **Cursor** I agree it's interesting that the AI consistently selects tasks 1 and 3 as dependencies. It found these tasks through the keyword matching (showing "Task 1: Implement Task Data Structure" and "Task 3: Implement Basic Task Operations"), but we should provide more comprehensive context to help it make better dependency selections. Let's enhance the task context by including task details for relevant tasks, not just their titles and descriptions: ```javascript // ... existing code ... taskMap[t.id] = { id: t.id, title: t.title, description: t.description, dependencies: t.dependencies || [], status: t.status }; }); // CLI-only feedback for the dependency analysis if (outputFormat === 'text') { console.log(boxen( chalk.cyan.bold('Task Context Analysis') + '\n', { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 0 }, borderColor: 'cyan', borderStyle: 'round' } )); } if (numericDependencies.length > 0) { // If specific dependencies were provided, focus on them const dependentTasks = data.tasks.filter((t) => numericDependencies.includes(t.id) ); contextTasks = `\nThis task depends on the following tasks:\n${dependentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; // Add more details about each dependency contextTasks += `\n\nDetailed information about dependencies:`; for (const depTask of dependentTasks) { contextTasks += `\n\n------ Task ${depTask.id}: ${depTask.title} ------\n`; contextTasks += `Description: ${depTask.description}\n`; contextTasks += `Status: ${depTask.status || 'pending'}\n`; contextTasks += `Priority: ${depTask.priority || 'medium'}\n`; // Add implementation details but truncate if too long if (depTask.details) { const truncatedDetails = depTask.details.length > 500 ? depTask.details.substring(0, 500) + '... (truncated)' : depTask.details; contextTasks += `Implementation Details: ${truncatedDetails}\n`; } } // Add dependency chain information (what these tasks depend on) const dependencyChains = []; for (const depTask of dependentTasks) { if (depTask.dependencies && depTask.dependencies.length > 0) { const subDeps = depTask.dependencies.map(subDepId => { const subDepTask = data.tasks.find(t => t.id === subDepId); return subDepTask ? `Task ${subDepId}: ${subDepTask.title}` : `Task ${subDepId}`; }); dependencyChains.push(`Task ${depTask.id} depends on: ${subDeps.join(', ')}`); } } if (dependencyChains.length > 0) { contextTasks += `\n\nDependency chains:\n${dependencyChains.join('\n')}`; } // Show dependency chains in CLI mode if (outputFormat === 'text' && dependentTasks.length > 0) { console.log(chalk.gray(` Explicitly specified dependencies:`)); dependentTasks.forEach(t => { console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); if (dependencyChains.length > 0) { console.log(chalk.gray(`\n Analyzing dependency chains:`)); dependencyChains.forEach(chain => { console.log(chalk.cyan(` • ${truncate(chain, 60)}`)); }); } console.log(); // Add spacing } } else { // If no dependencies provided, show recent and related tasks // Get recent tasks (newest first) const recentTasks = [...data.tasks] .sort((a, b) => b.id - a.id) .slice(0, 5); // Get tasks that might be related based on simple keyword matching const keywords = prompt.toLowerCase().split(/\s+/).filter(word => word.length > 4 && !['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word) ); // Get related tasks by keyword matching const relatedTasks = keywords.length > 0 ? data.tasks.filter(t => keywords.some(keyword => t.title.toLowerCase().includes(keyword) || t.description.toLowerCase().includes(keyword) ) && !recentTasks.some(rt => rt.id === t.id) ).slice(0, 5) // Increased from 3 to 5 : []; // Also look for tasks with similar purposes or categories const purposeCategories = [ {pattern: /(command|cli|flag)/i, label: "CLI commands"}, {pattern: /(task|subtask|add)/i, label: "Task management"}, {pattern: /(dependency|depend)/i, label: "Dependency handling"}, {pattern: /(AI|model|prompt)/i, label: "AI integration"}, {pattern: /(UI|display|show)/i, label: "User interface"} ]; const promptCategory = purposeCategories.find(cat => cat.pattern.test(prompt)); const categoryTasks = promptCategory ? data.tasks.filter(t => promptCategory.pattern.test(t.title) || promptCategory.pattern.test(t.description) || (t.details && promptCategory.pattern.test(t.details)) ) .filter(t => !recentTasks.some(rt => rt.id === t.id) && !relatedTasks.some(rt => rt.id === t.id) ) .slice(0, 3) : []; // Format basic task overviews if (recentTasks.length > 0) { contextTasks = `\nRecent tasks in the project:\n${recentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } if (relatedTasks.length > 0) { contextTasks += `\n\nPotentially related tasks based on keywords:\n${relatedTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } if (categoryTasks.length > 0) { contextTasks += `\n\nTasks related to ${promptCategory.label}:\n${categoryTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; } // Add detailed information about the most relevant tasks const allDetailedTasks = [...relatedTasks, ...categoryTasks.slice(0, 2), ...recentTasks.slice(0, 2)]; const uniqueDetailedTasks = Array.from(new Map(allDetailedTasks.map(t => [t.id, t])).values()).slice(0, 5); if (uniqueDetailedTasks.length > 0) { contextTasks += `\n\nDetailed information about relevant tasks:`; for (const task of uniqueDetailedTasks) { contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`; contextTasks += `Description: ${task.description}\n`; contextTasks += `Status: ${task.status || 'pending'}\n`; contextTasks += `Priority: ${task.priority || 'medium'}\n`; if (task.dependencies && task.dependencies.length > 0) { contextTasks += `Dependencies: ${task.dependencies.join(', ')}\n`; } // Add implementation details but truncate if too long if (task.details) { const truncatedDetails = task.details.length > 400 ? task.details.substring(0, 400) + '... (truncated)' : task.details; contextTasks += `Implementation Details: ${truncatedDetails}\n`; } } } // Add a concise view of the task dependency structure contextTasks += '\n\nSummary of task dependencies in the project:'; // Focus on pending tasks with the most similar purposes or categories // This helps identify patterns in how similar tasks are set up const relevantPendingTasks = promptCategory ? data.tasks .filter(t => t.status !== 'done' && (promptCategory.pattern.test(t.title) || promptCategory.pattern.test(t.description))) .slice(-10) : data.tasks.filter(t => t.status !== 'done').slice(-10); for (const task of relevantPendingTasks) { const depsStr = task.dependencies && task.dependencies.length > 0 ? task.dependencies.join(', ') : 'None'; contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; } // Additional analysis of common patterns const commandTasks = data.tasks.filter(t => /command|cli/i.test(t.title) || /command|cli/i.test(t.description)); if (commandTasks.length > 0 && /command|cli/i.test(prompt)) { contextTasks += '\n\nCommon patterns for command implementation tasks:'; const commandDeps = commandTasks .filter(t => t.dependencies && t.dependencies.length > 0) .map(t => t.dependencies) .flat(); // Count frequency of each dependency const depCounts = {}; commandDeps.forEach(dep => { depCounts[dep] = (depCounts[dep] || 0) + 1; }); // Get most common dependencies for command tasks const commonDeps = Object.entries(depCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 3); if (commonDeps.length > 0) { contextTasks += '\nMost common dependencies for similar tasks:'; commonDeps.forEach(([depId, count]) => { const depTask = data.tasks.find(t => t.id === parseInt(depId)); if (depTask) { contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`; } }); } } // Show keyword-based analysis in CLI mode if (outputFormat === 'text') { if (keywords.length > 0) { console.log(chalk.gray(` Keywords extracted for analysis: ${keywords.join(', ')}`)); if (relatedTasks.length > 0) { console.log(chalk.gray(`\n Potentially related tasks by keyword match:`)); relatedTasks.forEach(t => { console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); } } if (promptCategory && categoryTasks.length > 0) { console.log(chalk.gray(`\n Tasks related to ${promptCategory.label}:`)); categoryTasks.forEach(t => { console.log(chalk.magenta(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); } // Show the recent tasks used for context if (recentTasks.length > 0) { console.log(chalk.gray(`\n Using recent tasks for context:`)); recentTasks.slice(0, 3).forEach(t => { console.log(chalk.cyan(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); } // Show dependency structure visualization if (relevantPendingTasks.length > 0) { const depsToShow = Math.min(3, relevantPendingTasks.length); console.log(chalk.gray(`\n Task dependency patterns analyzed (${depsToShow} example${depsToShow > 1 ? 's' : ''}):`)); relevantPendingTasks.slice(0, depsToShow).forEach(task => { const depsStr = task.dependencies && task.dependencies.length > 0 ? chalk.yellow(task.dependencies.join(', ')) : chalk.gray('None'); console.log(chalk.blue(` • Task ${task.id} → [${depsStr}]`)); }); } // Add information about which tasks will be provided in detail if (uniqueDetailedTasks.length > 0) { console.log(chalk.gray(`\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:`)); uniqueDetailedTasks.forEach(t => { console.log(chalk.green(` • Task ${t.id}: ${truncate(t.title, 40)}`)); }); } console.log(); // Add spacing } } // System Prompt - Enhanced for dependency awareness // ... existing code ... ``` Now let's modify the task generation display to include context size information and make it more transparent about what's being sent to the AI: ```javascript // ... existing code ... // Add a visual transition to show we're moving to AI generation console.log(boxen( chalk.white.bold('AI Task Generation') + `\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + `\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + `\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` + `\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow( numericDependencies.length > 0 ? dependentTasks.length : (uniqueDetailedTasks ? uniqueDetailedTasks.length : 0) )}` + (promptCategory ? `\n${chalk.cyan('Task category: ')}${chalk.yellow(promptCategory.label)}` : '') + `\n${chalk.cyan('Dependency analysis depth: ')}${chalk.yellow('Full chain analysis')}`, { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 0, bottom: 1 } } )); loadingIndicator = startLoadingIndicator( `Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n` ); } try { // ... existing code ... ``` Now, let's update the system prompt to include specific guidance for the AI on how to make better dependency choices: ```javascript // ... existing code ... // System Prompt - Enhanced for dependency awareness const systemPrompt = "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + 'When determining dependencies for a new task, follow these principles:\n' + '1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' + '2. Prioritize task dependencies that are semantically related to the functionality being built.\n' + '3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n' + '4. Consider implementation details of candidate dependent tasks, not just titles.\n' + '5. Include the most specific dependencies rather than their parents when appropriate.\n' + '6. Focus on technical dependencies rather than organizational ones.'; // Task Structure Description (for user prompt) // ... existing code ... ``` Let's test the enhanced context and dependency analysis: --- **User** error --- **Cursor** I see the error - we're trying to use `uniqueDetailedTasks` in our display, but it's only defined in one code branch. Let's fix that: ```javascript // ... existing code ... // CLI-only feedback for the dependency analysis if (outputFormat === 'text') { console.log(boxen( chalk.cyan.bold('Task Context Analysis') + '\n', { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 0 }, borderColor: 'cyan', borderStyle: 'round' } )); } // Initialize variables that will be used in either branch let dependentTasks = []; let uniqueDetailedTasks = []; let promptCategory = null; if (numericDependencies.length > 0) { // If specific dependencies were provided, focus on them dependentTasks = data.tasks.filter((t) => numericDependencies.includes(t.id) ); // These are our detailed tasks when explicit dependencies are provided uniqueDetailedTasks = dependentTasks; contextTasks = `\nThis task depends on the following tasks:\n${dependentTasks .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .join('\n')}`; // ... existing code ... // Add a visual transition to show we're moving to AI generation console.log(boxen( chalk.white.bold('AI Task Generation') + `\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + `\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + `\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` + `\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(uniqueDetailedTasks.length)}` + (promptCategory ? `\n${chalk.cyan('Task category: ')}${chalk.yellow(promptCategory.label)}` : '') + `\n${chalk.cyan('Dependency analysis depth: ')}${chalk.yellow('Full chain analysis')}`, // ... existing code ... ``` Let's also fix the variable that's still defined inside the else branch instead of using our initialization: ```javascript // ... existing code ... const purposeCategories = [ {pattern: /(command|cli|flag)/i, label: "CLI commands"}, {pattern: /(task|subtask|add)/i, label: "Task management"}, {pattern: /(dependency|depend)/i, label: "Dependency handling"}, {pattern: /(AI|model|prompt)/i, label: "AI integration"}, {pattern: /(UI|display|show)/i, label: "User interface"} ]; promptCategory = purposeCategories.find(cat => cat.pattern.test(prompt)); const categoryTasks = promptCategory ? data.tasks.filter(t => promptCategory.pattern.test(t.title) || promptCategory.pattern.test(t.description) || (t.details && promptCategory.pattern.test(t.details)) ) // ... existing code ... ``` Let's test our fix: --- **User** despite ALL that, we still got 1 and 3 -- does that imply our previous implemenation was fine? we didnt use that many input tokens so im guessing no other tasks were added as dependencies. idk, maybe the issue is the way the keywords/search is working. it should be more fuzzy/smarter or rely more on the LLM even though we definitely would prefer to do as much as we can here to prepare the same information in smaller/condensed/curated format before passing it but in the end what really matters is to properly capture dependencies. it's possible we dont actually need to -- you can get the get-tasks tool taht you have access to get ALL tasks and make that determination for yourself so you can do a spot check --- **Cursor** You're right - despite our enhanced context and guidance, the AI still defaulted to those foundational tasks (1 and 3). It's particularly interesting that the output shows "Detailed tasks: 0" even though we were providing detailed information for 5 tasks. This suggests a few things: 1. There might still be an issue in how we're tracking `uniqueDetailedTasks` in the code. 2. The model might have a bias toward those early tasks since they are genuinely foundational (Task 1: "Implement Task Data Structure" and Task 3: "Implement Basic Task Operations"). 3. Our keyword matching might not be catching the truly relevant semantic connections between tasks. You've made an excellent point about letting the AI have more direct access to tasks. Here are two approaches we could take: 1. **AI-assisted pre-processing**: We could use an AI call to identify potential dependencies before the main task creation, specifically to analyze relevance. 2. **Direct access approach**: As you suggested, we could rely on the AI's ability to access all tasks directly through tools like `get_tasks`. This approach leverages the model's semantic understanding while giving it full access to the data. Would you like me to implement the fix for the "Detailed tasks: 0" issue, or would you prefer to move toward the direct access approach where the AI uses tools to gather its own context? --- **User** first, do a get-tasks to get that context --- **Cursor** Let me get all the tasks to better understand the context: --- **User** no. use the mcp tool get_tasks --- **Cursor** Let me get the tasks using the MCP tool: Now, let me get the task details for task ID 88 as you requested: Looking at the details of task #88 and the issue you mentioned, I'll now examine the current dependency handling in add-task.js: Let me examine more of the file to understand how dependencies are handled in the final part: Based on my analysis of the code and Task #88's requirements, let me identify how we can enhance the add-task functionality to better consider all task dependencies. Here's what I've found: 1. **Current Implementation**: - The add-task feature mainly considers explicitly provided dependencies (via CLI flags) or AI-suggested dependencies - It provides some context to the AI about dependency chains when explicit dependencies are provided - It does surface information about tasks with similar keywords, categories, or recent tasks when no explicit dependencies are provided - It has logic to validate dependencies and remove invalid ones 2. **Areas for Improvement**: - The current implementation doesn't recursively analyze the dependency structure beyond one level - When determining new task dependencies, it doesn't analyze how dependencies of dependencies might affect the new task - It could provide more comprehensive dependency chain information to the AI model - The system could better detect and suggest indirect dependencies automatically Let me implement these improvements to enhance the add-task functionality: ```javascript // ... existing code ... // Create a consistent logFn object regardless of context const logFn = isMCP ? mcpLog // Use MCP logger if provided : { // Create a wrapper around consoleLog for CLI info: (...args) => consoleLog('info', ...args), warn: (...args) => consoleLog('warn', ...args), error: (...args) => consoleLog('error', ...args), debug: (...args) => consoleLog('debug', ...args), success: (...args) => consoleLog('success', ...args) }; const effectivePriority = priority || getDefaultPriority(projectRoot); logFn.info( `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` ); let loadingIndicator = null; let aiServiceResponse = null; // To store the full response from AI service // Create custom reporter that checks for MCP log const report = (message, level = 'info') => { if (mcpLog) { mcpLog[level](message); } else if (outputFormat === 'text') { consoleLog(level, message); } }; /** * Recursively builds a dependency graph for a given task * @param {Array} tasks - All tasks from tasks.json * @param {number} taskId - ID of the task to analyze * @param {Set} visited - Set of already visited task IDs * @param {Map} depthMap - Map of task ID to its depth in the graph * @param {number} depth - Current depth in the recursion * @return {Object} Dependency graph data */ function buildDependencyGraph(tasks, taskId, visited = new Set(), depthMap = new Map(), depth = 0) { // Skip if we've already visited this task or it doesn't exist if (visited.has(taskId)) { return null; } // Find the task const task = tasks.find(t => t.id === taskId); if (!task) { return null; } // Mark as visited visited.add(taskId); // Update depth if this is a deeper path to this task if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { depthMap.set(taskId, depth); } // Process dependencies const dependencyData = []; if (task.dependencies && task.dependencies.length > 0) { for (const depId of task.dependencies) { const depData = buildDependencyGraph(tasks, depId, visited, depthMap, depth + 1); if (depData) { dependencyData.push(depData); } } } return { id: task.id, title: task.title, description: task.description, status: task.status, dependencies: dependencyData }; } try { // Read the existing tasks const data = readJSON(tasksPath); if (!data || !data.tasks) { report('Invalid or missing tasks.json.', 'error'); throw new Error('Invalid or missing tasks.json.'); } // Find the highest task ID to determine the next ID const highestId = data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0; const newTaskId = highestId + 1; // Only show UI box for CLI mode if (outputFormat === 'text') { console.log( boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }) ); } // Validate dependencies before proceeding const invalidDeps = dependencies.filter((depId) => { // Ensure depId is parsed as a number for comparison const numDepId = parseInt(depId, 10); return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); }); if (invalidDeps.length > 0) { report( `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, 'warn' ); report('Removing invalid dependencies...', 'info'); dependencies = dependencies.filter( (depId) => !invalidDeps.includes(depId) ); } // Ensure dependencies are numbers const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); // Build dependency graphs for explicitly specified dependencies const dependencyGraphs = []; const allRelatedTaskIds = new Set(); const depthMap = new Map(); // First pass: build a complete dependency graph for each specified dependency for (const depId of numericDependencies) { const graph = buildDependencyGraph(data.tasks, depId, new Set(), depthMap); if (graph) { dependencyGraphs.push(graph); } } // Second pass: build a set of all related task IDs for flat analysis for (const [taskId, depth] of depthMap.entries()) { allRelatedTaskIds.add(taskId); } let taskData; // Check if manual task data is provided if (manualTaskData) { report('Using manually provided task data', 'info'); taskData = manualTaskData; report('DEBUG: Taking MANUAL task data path.', 'debug'); // Basic validation for manual data if ( !taskData.title || typeof taskData.title !== 'string' || !taskData.description || typeof taskData.description !== 'string' ) { throw new Error( 'Manual task data must include at least a title and description.' ); } } else { report('DEBUG: Taking AI task generation path.', 'debug'); // --- Refactored AI Interaction --- report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); // Create context string for task creation prompt let contextTasks = ''; // Create a dependency map for better understanding of the task relationships const taskMap = {}; data.tasks.forEach(t => { // For each task, only include id, title, description, and dependencies taskMap[t.id] = { id: t.id, title: t.title, description: t.description, dependencies: t.dependencies || [], status: t.status }; }); // CLI-only feedback for the dependency analysis if (outputFormat === 'text') { console.log(boxen( chalk.cyan.bold('Task Context Analysis') + '\n', { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 0 }, borderColor: 'cyan', borderStyle: 'round' } )); } // Initialize variables that will be used in either branch let dependentTasks = []; let uniqueDetailedTasks = []; let promptCategory = null; if (numericDependencies.length > 0) { // If specific dependencies were provided, focus on them // Get all tasks that were found in the dependency graph dependentTasks = Array.from(allRelatedTaskIds).map(id => data.tasks.find(t => t.id === id) ).filter(Boolean); // Sort by depth in the dependency chain dependentTasks.sort((a, b) => { const depthA = depthMap.get(a.id) || 0; const depthB = depthMap.get(b.id) || 0; return depthA - depthB; // Lowest depth (root dependencies) first }); // Limit the number of detailed tasks to avoid context explosion uniqueDetailedTasks = dependentTasks.slice(0, 8); contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`; const directDeps = data.tasks.filter(t => numericDependencies.includes(t.id)); contextTasks += `\n${directDeps.map(t => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`; // Add an overview of indirect dependencies if present const indirectDeps = dependentTasks.filter(t => !numericDependencies.includes(t.id)); if (indirectDeps.length > 0) { contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`; contextTasks += `\n${indirectDeps.slice(0, 5).map(t => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`; if (indirectDeps.length > 5) { contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; } } // Add more details about each dependency, prioritizing direct dependencies contextTasks += `\n\nDetailed information about dependencies:`; for (const depTask of uniqueDetailedTasks) { const depthInfo = depthMap.get(depTask.id) ? ` (depth: ${depthMap.get(depTask.id)})` : ''; const isDirect = numericDependencies.includes(depTask.id) ? " [DIRECT DEPENDENCY]" : ""; contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`; contextTasks += `Description: ${depTask.description}\n`; contextTasks += `Status: ${depTask.status || 'pending'}\n`; contextTasks += `Priority: ${depTask.priority || 'medium'}\n`; // List its dependencies if (depTask.dependencies && depTask.dependencies.length > 0) { const depDeps = depTask.dependencies.map(dId => { const depDepTask = data.tasks.find(t => t.id === dId); return depDepTask ? `Task ${dId}: ${depDepTask.title}` : `Task ${dId}`; }); contextTasks += `Dependencies: ${depDeps.join(', ')}\n`; } else { contextTasks += `Dependencies: None\n`; } // Add implementation details but truncate if too long if (depTask.details) { const truncatedDetails = depTask.details.length > 400 ? depTask.details.substring(0, 400) + '... (truncated)' : depTask.details; contextTasks += `Implementation Details: ${truncatedDetails}\n`; } } // Add dependency chain visualization if (dependencyGraphs.length > 0) { contextTasks += '\n\nDependency Chain Visualization:'; // Helper function to format dependency chain as text function formatDependencyChain(node, prefix = '', isLast = true, depth = 0) { if (depth > 3) return ''; // Limit depth to avoid excessive nesting const connector = isLast ? '└── ' : '├── '; const childPrefix = isLast ? ' ' : '│ '; let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; if (node.dependencies && node.dependencies.length > 0) { for (let i = 0; i < node.dependencies.length; i++) { const isLastChild = i === node.dependencies.length - 1; result += formatDependencyChain( node.dependencies[i], prefix + childPrefix, isLastChild, depth + 1 ); } } return result; } // Format each dependency graph for (const graph of dependencyGraphs) { contextTasks += formatDependencyChain(graph); } } // Show dependency analysis in CLI mode if (outputFormat === 'text') { if (directDeps.length > 0) { console.log(chalk.gray(` Explicitly specified dependencies:`)); directDeps.forEach(t => { console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)); }); } if (indirectDeps.length > 0) { console.log(chalk.gray(`\n Indirect dependencies (${indirectDeps.length} total):`)); indirectDeps.slice(0, 3).forEach(t => { const depth = depthMap.get(t.id) || 0; console.log(chalk.cyan(` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}`)); }); if (indirectDeps.length > 3) { console.log(chalk.cyan(` • ... and ${indirectDeps.length - 3} more indirect dependencies`)); } } // Visualize the dependency chain if (dependencyGraphs.length > 0) { console.log(chalk.gray(`\n Dependency chain visualization:`)); // Convert dependency graph to ASCII art for terminal function visualizeDependencyGraph(node, prefix = '', isLast = true, depth = 0) { if (depth > 2) return; // Limit depth for display const connector = isLast ? '└── ' : '├── '; const childPrefix = isLast ? ' ' : '│ '; console.log(chalk.blue(` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}`)); if (node.dependencies && node.dependencies.length > 0) { for (let i = 0; i < node.dependencies.length; i++) { const isLastChild = i === node.dependencies.length - 1; visualizeDependencyGraph( node.dependencies[i], prefix + childPrefix, isLastChild, depth + 1 ); } } } // Visualize each dependency graph for (const graph of dependencyGraphs) { visualizeDependencyGraph(graph); } } console.log(); // Add spacing } } else { // ... existing code ... } // ... existing code ... // Create the new task object const newTask = { id: newTaskId, title: taskData.title, description: taskData.description, details: taskData.details || '', testStrategy: taskData.testStrategy || '', status: 'pending', dependencies: taskData.dependencies?.length ? taskData.dependencies : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified priority: effectivePriority, subtasks: [] // Initialize with empty subtasks array }; // Additional check: validate all dependencies in the AI response if (taskData.dependencies?.length) { const allValidDeps = taskData.dependencies.every(depId => { const numDepId = parseInt(depId, 10); return !isNaN(numDepId) && data.tasks.some(t => t.id === numDepId); }); if (!allValidDeps) { report('AI suggested invalid dependencies. Filtering them out...', 'warn'); newTask.dependencies = taskData.dependencies.filter(depId => { const numDepId = parseInt(depId, 10); return !isNaN(numDepId) && data.tasks.some(t => t.id === numDepId); }); } } // Add the task to the tasks array data.tasks.push(newTask); // ... existing code ... ``` Now, let's update the system prompt to better guide the AI in making accurate dependency recommendations using the enhanced dependency information we're providing: ```javascript // ... existing code ... // System Prompt - Enhanced for dependency awareness const systemPrompt = "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + 'When determining dependencies for a new task, follow these principles:\n' + '1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' + '2. Prioritize task dependencies that are semantically related to the functionality being built.\n' + '3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n' + '4. Consider implementation details of candidate dependent tasks, not just titles.\n' + '5. Include the most specific dependencies rather than their parents when appropriate.\n' + '6. Focus on technical dependencies rather than organizational ones.'; // ... existing code to ... // System Prompt - Enhanced for comprehensive dependency awareness const systemPrompt = "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + 'When determining dependencies for a new task, follow these principles:\n' + '1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' + '2. Analyze the entire dependency chain to find the most specific and direct dependencies.\n' + '3. Consider both direct and indirect (transitive) dependencies - a task may depend on another task that depends on yet another task.\n' + '4. When possible, reference the most specific or leaf-node tasks in a dependency chain rather than their parents.\n' + '5. Prioritize task dependencies that are semantically related to the functionality being built.\n' + '6. Consider the depth of the dependency chain - tasks deeper in the chain might be more specific.\n' + '7. Respect the existing dependency structure in the project to maintain consistency.\n' + '8. Avoid defaulting to early foundational tasks (like tasks #1-3) unless absolutely necessary.\n' + '9. Consider implementation details of candidate dependent tasks, not just their titles.\n' + '10. Balance between too many dependencies (overly constrained) and too few (missing prerequisite work).\n' + "11. Pay attention to tasks marked as 'done' vs 'pending' when considering dependencies.\n" + '12. Base dependency decisions on technical requirements rather than organizational preferences.'; // ... existing code ... ``` Now, let's update the user prompt to utilize the enhanced dependency information more effectively: ```javascript // ... existing code ... // User Prompt const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the information about existing tasks and their dependencies, include appropriate dependencies in the "dependencies" array if any are needed. Only include task IDs that this new task directly depends on. Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific. Don't include the task ID in the title.`; // ... existing code to ... // User Prompt - Enhanced for better dependency analysis const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the detailed information about existing tasks and their dependency relationships, carefully consider which dependencies this new task should have: 1. ANALYZE: Review the entire dependency structure provided above, including both direct and indirect dependencies. 2. IDENTIFY: Determine which specific existing tasks must be completed before this new task can begin. 3. CONSIDER: Think about both technical requirements and logical task ordering. 4. DECIDE: Select only the most direct, specific dependencies - avoid including a parent task if you're already including its child task. 5. VALIDATE: Ensure all selected dependencies are necessary and sufficient for this task to begin. Include these carefully selected task IDs in the "dependencies" array of your response. If there are no logical dependencies, return an empty array []. Return your answer as a single JSON object matching the schema precisely: ${taskStructureDesc} Make sure the details and test strategy are thorough and specific. Don't include the task ID in the title.`; // ... existing code ... ``` Let's also enhance the CLI output to display dependency information for the newly created task: ```javascript // ... existing code ... // Check if AI added new dependencies that weren't explicitly provided const aiAddedDeps = newTask.dependencies.filter( (dep) => !numericDependencies.includes(dep) ); // Show success message box console.log( boxen( chalk.white.bold(`Task ${newTaskId} Created Successfully`) + '\n\n' + chalk.white(`Title: ${newTask.title}`) + '\n' + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + '\n' + chalk.white( `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` ) + '\n' + (newTask.dependencies.length > 0 ? chalk.white(`Dependencies: ${newTask.dependencies.join(', ')}`) + (aiAddedDeps.length > 0 ? chalk.gray(` (${aiAddedDeps.length} auto-detected by AI)`) : '') + '\n' : '') + '\n' + chalk.white.bold('Next Steps:') + '\n' + chalk.cyan( `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` ) + '\n' + chalk.cyan( `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` ) + '\n' + chalk.cyan( `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` ), { padding: 1, borderColor: 'green', borderStyle: 'round' } ) ); // ... existing code to ... // Check if AI added new dependencies that weren't explicitly provided const aiAddedDeps = newTask.dependencies.filter( (dep) => !numericDependencies.includes(dep) ); // Check if AI removed any dependencies that were explicitly provided const aiRemovedDeps = numericDependencies.filter( (dep) => !newTask.dependencies.includes(dep) ); // Get task titles for dependencies to display const depTitles = {}; newTask.dependencies.forEach((dep) => { const depTask = data.tasks.find((t) => t.id === dep); if (depTask) { depTitles[dep] = truncate(depTask.title, 30); } }); // Prepare dependency display string let dependencyDisplay = ''; if (newTask.dependencies.length > 0) { dependencyDisplay = chalk.white('Dependencies:') + '\n'; newTask.dependencies.forEach((dep) => { const isAiAdded = aiAddedDeps.includes(dep); const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; dependencyDisplay += chalk.white(` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`) + '\n'; }); } else { dependencyDisplay = chalk.white('Dependencies: None') + '\n'; } // Add info about removed dependencies if any if (aiRemovedDeps.length > 0) { dependencyDisplay += chalk.gray('\nUser-specified dependencies that were not used:') + '\n'; aiRemovedDeps.forEach((dep) => { const depTask = data.tasks.find((t) => t.id === dep); const title = depTask ? truncate(depTask.title, 30) : 'Unknown task'; dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n'; }); } // Add dependency analysis summary let dependencyAnalysis = ''; if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { dependencyAnalysis = '\n' + chalk.white.bold('Dependency Analysis:') + '\n'; if (aiAddedDeps.length > 0) { dependencyAnalysis += chalk.green( `AI identified ${aiAddedDeps.length} additional dependencies` ) + '\n'; } if (aiRemovedDeps.length > 0) { dependencyAnalysis += chalk.yellow( `AI excluded ${aiRemovedDeps.length} user-provided dependencies` ) + '\n'; } } // Show success message box console.log( boxen( chalk.white.bold(`Task ${newTaskId} Created Successfully`) + '\n\n' + chalk.white(`Title: ${newTask.title}`) + '\n' + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + '\n' + chalk.white( `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` ) + '\n\n' + dependencyDisplay + dependencyAnalysis + '\n' + chalk.white.bold('Next Steps:') + '\n' + chalk.cyan( `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` ) + '\n' + chalk.cyan( `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` ) + '\n' + chalk.cyan( `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` ), { padding: 1, borderColor: 'green', borderStyle: 'round' } ) ); // ... existing code ... ``` Let's test our changes to see how they improve the task dependency handling: ``` -------------------------------------------------------------------------------- /context/fastmcp-docs.txt: -------------------------------------------------------------------------------- ``` Directory Structure: └── ./ ├── src │ ├── bin │ │ └── fastmcp.ts │ ├── examples │ │ └── addition.ts │ ├── FastMCP.test.ts │ └── FastMCP.ts ├── eslint.config.js ├── package.json ├── README.md └── vitest.config.js --- File: /src/bin/fastmcp.ts --- #!/usr/bin/env node import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { execa } from "execa"; await yargs(hideBin(process.argv)) .scriptName("fastmcp") .command( "dev <file>", "Start a development server", (yargs) => { return yargs.positional("file", { type: "string", describe: "The path to the server file", demandOption: true, }); }, async (argv) => { try { await execa({ stdin: "inherit", stdout: "inherit", stderr: "inherit", })`npx @wong2/mcp-cli npx tsx ${argv.file}`; } catch { process.exit(1); } }, ) .command( "inspect <file>", "Inspect a server file", (yargs) => { return yargs.positional("file", { type: "string", describe: "The path to the server file", demandOption: true, }); }, async (argv) => { try { await execa({ stdout: "inherit", stderr: "inherit", })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; } catch { process.exit(1); } }, ) .help() .parseAsync(); --- File: /src/examples/addition.ts --- /** * This is a complete example of an MCP server. */ import { FastMCP } from "../FastMCP.js"; import { z } from "zod"; const server = new FastMCP({ name: "Addition", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: "Example log content", }; }, }); server.addPrompt({ name: "git-commit", description: "Generate a Git commit message", arguments: [ { name: "changes", description: "Git diff or description of changes", required: true, }, ], load: async (args) => { return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, }); server.start({ transportType: "stdio", }); --- File: /src/FastMCP.test.ts --- import { FastMCP, FastMCPSession, UserError, imageContent } from "./FastMCP.js"; import { z } from "zod"; import { test, expect, vi } from "vitest"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { getRandomPort } from "get-port-please"; import { setTimeout as delay } from "timers/promises"; import { CreateMessageRequestSchema, ErrorCode, ListRootsRequestSchema, LoggingMessageNotificationSchema, McpError, PingRequestSchema, Root, } from "@modelcontextprotocol/sdk/types.js"; import { createEventSource, EventSourceClient } from 'eventsource-client'; const runWithTestServer = async ({ run, client: createClient, server: createServer, }: { server?: () => Promise<FastMCP>; client?: () => Promise<Client>; run: ({ client, server, }: { client: Client; server: FastMCP; session: FastMCPSession; }) => Promise<void>; }) => { const port = await getRandomPort(); const server = createServer ? await createServer() : new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); try { const client = createClient ? await createClient() : new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); const session = await new Promise<FastMCPSession>((resolve) => { server.on("connect", (event) => { resolve(event.session); }); client.connect(transport); }); await run({ client, server, session }); } finally { await server.stop(); } return port; }; test("adds tools", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { expect(await client.listTools()).toEqual({ tools: [ { name: "add", description: "Add two numbers", inputSchema: { additionalProperties: false, $schema: "http://json-schema.org/draft-07/schema#", type: "object", properties: { a: { type: "number" }, b: { type: "number" }, }, required: ["a", "b"], }, }, ], }); }, }); }); test("calls a tool", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "3" }], }); }, }); }); test("returns a list", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async () => { return { content: [ { type: "text", text: "a" }, { type: "text", text: "b" }, ], }; }, }); return server; }, run: async ({ client }) => { expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [ { type: "text", text: "a" }, { type: "text", text: "b" }, ], }); }, }); }); test("returns an image", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async () => { return imageContent({ buffer: Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64", ), }); }, }); return server; }, run: async ({ client }) => { expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [ { type: "image", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "image/png", }, ], }); }, }); }); test("handles UserError errors", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async () => { throw new UserError("Something went wrong"); }, }); return server; }, run: async ({ client }) => { expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "Something went wrong" }], isError: true, }); }, }); }); test("calling an unknown tool throws McpError with MethodNotFound code", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); return server; }, run: async ({ client }) => { try { await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.MethodNotFound); } }, }); }); test("tracks tool progress", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args, { reportProgress }) => { reportProgress({ progress: 0, total: 10, }); await delay(100); return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { const onProgress = vi.fn(); await client.callTool( { name: "add", arguments: { a: 1, b: 2, }, }, undefined, { onprogress: onProgress, }, ); expect(onProgress).toHaveBeenCalledTimes(1); expect(onProgress).toHaveBeenCalledWith({ progress: 0, total: 10, }); }, }); }); test("sets logging levels", async () => { await runWithTestServer({ run: async ({ client, session }) => { await client.setLoggingLevel("debug"); expect(session.loggingLevel).toBe("debug"); await client.setLoggingLevel("info"); expect(session.loggingLevel).toBe("info"); }, }); }); test("sends logging messages to the client", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args, { log }) => { log.debug("debug message", { foo: "bar", }); log.error("error message"); log.info("info message"); log.warn("warn message"); return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { const onLog = vi.fn(); client.setNotificationHandler( LoggingMessageNotificationSchema, (message) => { if (message.method === "notifications/message") { onLog({ level: message.params.level, ...(message.params.data ?? {}), }); } }, ); await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }); expect(onLog).toHaveBeenCalledTimes(4); expect(onLog).toHaveBeenNthCalledWith(1, { level: "debug", message: "debug message", context: { foo: "bar", }, }); expect(onLog).toHaveBeenNthCalledWith(2, { level: "error", message: "error message", }); expect(onLog).toHaveBeenNthCalledWith(3, { level: "info", message: "info message", }); expect(onLog).toHaveBeenNthCalledWith(4, { level: "warning", message: "warn message", }); }, }); }); test("adds resources", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: "Example log content", }; }, }); return server; }, run: async ({ client }) => { expect(await client.listResources()).toEqual({ resources: [ { uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", }, ], }); }, }); }); test("clients reads a resource", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: "Example log content", }; }, }); return server; }, run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { uri: "file:///logs/app.log", name: "Application Logs", text: "Example log content", mimeType: "text/plain", }, ], }); }, }); }); test("clients reads a resource that returns multiple resources", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return [ { text: "a", }, { text: "b", }, ]; }, }); return server; }, run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { uri: "file:///logs/app.log", name: "Application Logs", text: "a", mimeType: "text/plain", }, { uri: "file:///logs/app.log", name: "Application Logs", text: "b", mimeType: "text/plain", }, ], }); }, }); }); test("adds prompts", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ name: "git-commit", description: "Generate a Git commit message", arguments: [ { name: "changes", description: "Git diff or description of changes", required: true, }, ], load: async (args) => { return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, }); return server; }, run: async ({ client }) => { expect( await client.getPrompt({ name: "git-commit", arguments: { changes: "foo", }, }), ).toEqual({ description: "Generate a Git commit message", messages: [ { role: "user", content: { type: "text", text: "Generate a concise but descriptive commit message for these changes:\n\nfoo", }, }, ], }); expect(await client.listPrompts()).toEqual({ prompts: [ { name: "git-commit", description: "Generate a Git commit message", arguments: [ { name: "changes", description: "Git diff or description of changes", required: true, }, ], }, ], }); }, }); }); test("uses events to notify server of client connect/disconnect", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); const onConnect = vi.fn(); const onDisconnect = vi.fn(); server.on("connect", onConnect); server.on("disconnect", onDisconnect); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client.connect(transport); await delay(100); expect(onConnect).toHaveBeenCalledTimes(1); expect(onDisconnect).toHaveBeenCalledTimes(0); expect(server.sessions).toEqual([expect.any(FastMCPSession)]); await client.close(); await delay(100); expect(onConnect).toHaveBeenCalledTimes(1); expect(onDisconnect).toHaveBeenCalledTimes(1); await server.stop(); }); test("handles multiple clients", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const client1 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport1 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client1.connect(transport1); const client2 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport2 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client2.connect(transport2); await delay(100); expect(server.sessions).toEqual([ expect.any(FastMCPSession), expect.any(FastMCPSession), ]); await server.stop(); }); test("session knows about client capabilities", async () => { await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: [ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, ], }; }); return client; }, run: async ({ session }) => { expect(session.clientCapabilities).toEqual({ roots: { listChanged: true, }, }); }, }); }); test("session knows about roots", async () => { await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: [ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, ], }; }); return client; }, run: async ({ session }) => { expect(session.roots).toEqual([ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, ]); }, }); }); test("session listens to roots changes", async () => { let clientRoots: Root[] = [ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, ]; await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: clientRoots, }; }); return client; }, run: async ({ session, client }) => { expect(session.roots).toEqual([ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, ]); clientRoots.push({ uri: "file:///home/user/projects/backend", name: "Backend Repository", }); await client.sendRootsListChanged(); const onRootsChanged = vi.fn(); session.on("rootsChanged", onRootsChanged); await delay(100); expect(session.roots).toEqual([ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, { uri: "file:///home/user/projects/backend", name: "Backend Repository", }, ]); expect(onRootsChanged).toHaveBeenCalledTimes(1); expect(onRootsChanged).toHaveBeenCalledWith({ roots: [ { uri: "file:///home/user/projects/frontend", name: "Frontend Repository", }, { uri: "file:///home/user/projects/backend", name: "Backend Repository", }, ], }); }, }); }); test("session sends pings to the client", async () => { await runWithTestServer({ run: async ({ client }) => { const onPing = vi.fn().mockReturnValue({}); client.setRequestHandler(PingRequestSchema, onPing); await delay(2000); expect(onPing).toHaveBeenCalledTimes(1); }, }); }); test("completes prompt arguments", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, arguments: [ { name: "name", description: "Name of the country", required: true, complete: async (value) => { if (value === "Germ") { return { values: ["Germany"], }; } return { values: [], }; }, }, ], }); return server; }, run: async ({ client }) => { const response = await client.complete({ ref: { type: "ref/prompt", name: "countryPoem", }, argument: { name: "name", value: "Germ", }, }); expect(response).toEqual({ completion: { values: ["Germany"], }, }); }, }); }); test("adds automatic prompt argument completion when enum is provided", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, arguments: [ { name: "name", description: "Name of the country", required: true, enum: ["Germany", "France", "Italy"], }, ], }); return server; }, run: async ({ client }) => { const response = await client.complete({ ref: { type: "ref/prompt", name: "countryPoem", }, argument: { name: "name", value: "Germ", }, }); expect(response).toEqual({ completion: { values: ["Germany"], total: 1, }, }); }, }); }); test("completes template resource arguments", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ uriTemplate: "issue:///{issueId}", name: "Issue", mimeType: "text/plain", arguments: [ { name: "issueId", description: "ID of the issue", complete: async (value) => { if (value === "123") { return { values: ["123456"], }; } return { values: [], }; }, }, ], load: async ({ issueId }) => { return { text: `Issue ${issueId}`, }; }, }); return server; }, run: async ({ client }) => { const response = await client.complete({ ref: { type: "ref/resource", uri: "issue:///{issueId}", }, argument: { name: "issueId", value: "123", }, }); expect(response).toEqual({ completion: { values: ["123456"], }, }); }, }); }); test("lists resource templates", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", required: true, }, ], load: async ({ name }) => { return { text: `Example log content for ${name}`, }; }, }); return server; }, run: async ({ client }) => { expect(await client.listResourceTemplates()).toEqual({ resourceTemplates: [ { name: "Application Logs", uriTemplate: "file:///logs/{name}.log", }, ], }); }, }); }); test("clients reads a resource accessed via a resource template", async () => { const loadSpy = vi.fn((_args) => { return { text: "Example log content", }; }); await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", }, ], async load(args) { return loadSpy(args); }, }); return server; }, run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { uri: "file:///logs/app.log", name: "Application Logs", text: "Example log content", mimeType: "text/plain", }, ], }); expect(loadSpy).toHaveBeenCalledWith({ name: "app", }); }, }); }); test("makes a sampling request", async () => { const onMessageRequest = vi.fn(() => { return { model: "gpt-3.5-turbo", role: "assistant", content: { type: "text", text: "The files are in the current directory.", }, }; }); await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { sampling: {}, }, }, ); return client; }, run: async ({ client, session }) => { client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest); const response = await session.requestSampling({ messages: [ { role: "user", content: { type: "text", text: "What files are in the current directory?", }, }, ], systemPrompt: "You are a helpful file system assistant.", includeContext: "thisServer", maxTokens: 100, }); expect(response).toEqual({ model: "gpt-3.5-turbo", role: "assistant", content: { type: "text", text: "The files are in the current directory.", }, }); expect(onMessageRequest).toHaveBeenCalledTimes(1); }, }); }); test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { try { await client.callTool({ name: "add", arguments: { a: 1, b: "invalid", }, }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters"); } }, }); }); test("server remains usable after InvalidParams error", async () => { await runWithTestServer({ server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); return server; }, run: async ({ client }) => { try { await client.callTool({ name: "add", arguments: { a: 1, b: "invalid", }, }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters"); } expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "3" }], }); }, }); }); test("allows new clients to connect after a client disconnects", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const client1 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport1 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client1.connect(transport1); expect( await client1.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "3" }], }); await client1.close(); const client2 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport2 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client2.connect(transport2); expect( await client2.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "3" }], }); await client2.close(); await server.stop(); }); test("able to close server immediately after starting it", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); // We were previously not waiting for the server to start. // Therefore, this would have caused error 'Server is not running.'. await server.stop(); }); test("closing event source does not produce error", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const eventSource = await new Promise<EventSourceClient>((onMessage) => { const eventSource = createEventSource({ onConnect: () => { console.info('connected'); }, onDisconnect: () => { console.info('disconnected'); }, onMessage: () => { onMessage(eventSource); }, url: `http://127.0.0.1:${port}/sse`, }); }); expect(eventSource.readyState).toBe('open'); eventSource.close(); // We were getting unhandled error 'Not connected' // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52 await delay(1000); await server.stop(); }); test("provides auth to tools", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { id: 1, }; }); const server = new FastMCP<{id: number}>({ name: "Test", version: "1.0.0", authenticate, }); const execute = vi.fn(async (args) => { return String(args.a + args.b); }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute, }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); expect(authenticate, "authenticate should have been called").toHaveBeenCalledTimes(1); expect( await client.callTool({ name: "add", arguments: { a: 1, b: 2, }, }), ).toEqual({ content: [{ type: "text", text: "3" }], }); expect(execute, "execute should have been called").toHaveBeenCalledTimes(1); expect(execute).toHaveBeenCalledWith({ a: 1, b: 2, }, { log: { debug: expect.any(Function), error: expect.any(Function), info: expect.any(Function), warn: expect.any(Function), }, reportProgress: expect.any(Function), session: { id: 1 }, }); }); test("blocks unauthorized requests", async () => { const port = await getRandomPort(); const server = new FastMCP<{id: number}>({ name: "Test", version: "1.0.0", authenticate: async () => { throw new Response(null, { status: 401, statusText: "Unauthorized", }); }, }); await server.start({ transportType: "sse", sse: { endpoint: "/sse", port, }, }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); expect(async () => { await client.connect(transport); }).rejects.toThrow("SSE error: Non-200 status code (401)"); }); --- File: /src/FastMCP.ts --- import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ClientCapabilities, CompleteRequestSchema, CreateMessageRequestSchema, ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, Root, RootsListChangedNotificationSchema, ServerCapabilities, SetLevelRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import { z } from "zod"; import { setTimeout as delay } from "timers/promises"; import { readFile } from "fs/promises"; import { fileTypeFromBuffer } from "file-type"; import { StrictEventEmitter } from "strict-event-emitter-types"; import { EventEmitter } from "events"; import Fuse from "fuse.js"; import { startSSEServer } from "mcp-proxy"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import parseURITemplate from "uri-templates"; import http from "http"; import { fetch } from "undici"; export type SSEServer = { close: () => Promise<void>; }; type FastMCPEvents<T extends FastMCPSessionAuth> = { connect: (event: { session: FastMCPSession<T> }) => void; disconnect: (event: { session: FastMCPSession<T> }) => void; }; type FastMCPSessionEvents = { rootsChanged: (event: { roots: Root[] }) => void; error: (event: { error: Error }) => void; }; /** * Generates an image content object from a URL, file path, or buffer. */ export const imageContent = async ( input: { url: string } | { path: string } | { buffer: Buffer }, ): Promise<ImageContent> => { let rawData: Buffer; if ("url" in input) { const response = await fetch(input.url); if (!response.ok) { throw new Error(`Failed to fetch image from URL: ${response.statusText}`); } rawData = Buffer.from(await response.arrayBuffer()); } else if ("path" in input) { rawData = await readFile(input.path); } else if ("buffer" in input) { rawData = input.buffer; } else { throw new Error( "Invalid input: Provide a valid 'url', 'path', or 'buffer'", ); } const mimeType = await fileTypeFromBuffer(rawData); const base64Data = rawData.toString("base64"); return { type: "image", data: base64Data, mimeType: mimeType?.mime ?? "image/png", } as const; }; abstract class FastMCPError extends Error { public constructor(message?: string) { super(message); this.name = new.target.name; } } type Extra = unknown; type Extras = Record<string, Extra>; export class UnexpectedStateError extends FastMCPError { public extras?: Extras; public constructor(message: string, extras?: Extras) { super(message); this.name = new.target.name; this.extras = extras; } } /** * An error that is meant to be surfaced to the user. */ export class UserError extends UnexpectedStateError {} type ToolParameters = z.ZodTypeAny; type Literal = boolean | null | number | string | undefined; type SerializableValue = | Literal | SerializableValue[] | { [key: string]: SerializableValue }; type Progress = { /** * The progress thus far. This should increase every time progress is made, even if the total is unknown. */ progress: number; /** * Total number of items to process (or total progress required), if known. */ total?: number; }; type Context<T extends FastMCPSessionAuth> = { session: T | undefined; reportProgress: (progress: Progress) => Promise<void>; log: { debug: (message: string, data?: SerializableValue) => void; error: (message: string, data?: SerializableValue) => void; info: (message: string, data?: SerializableValue) => void; warn: (message: string, data?: SerializableValue) => void; }; }; type TextContent = { type: "text"; text: string; }; const TextContentZodSchema = z .object({ type: z.literal("text"), /** * The text content of the message. */ text: z.string(), }) .strict() satisfies z.ZodType<TextContent>; type ImageContent = { type: "image"; data: string; mimeType: string; }; const ImageContentZodSchema = z .object({ type: z.literal("image"), /** * The base64-encoded image data. */ data: z.string().base64(), /** * The MIME type of the image. Different providers may support different image types. */ mimeType: z.string(), }) .strict() satisfies z.ZodType<ImageContent>; type Content = TextContent | ImageContent; const ContentZodSchema = z.discriminatedUnion("type", [ TextContentZodSchema, ImageContentZodSchema, ]) satisfies z.ZodType<Content>; type ContentResult = { content: Content[]; isError?: boolean; }; const ContentResultZodSchema = z .object({ content: ContentZodSchema.array(), isError: z.boolean().optional(), }) .strict() satisfies z.ZodType<ContentResult>; type Completion = { values: string[]; total?: number; hasMore?: boolean; }; /** * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 */ const CompletionZodSchema = z.object({ /** * An array of completion values. Must not exceed 100 items. */ values: z.array(z.string()).max(100), /** * The total number of completion options available. This can exceed the number of values actually sent in the response. */ total: z.optional(z.number().int()), /** * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. */ hasMore: z.optional(z.boolean()), }) satisfies z.ZodType<Completion>; type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = { name: string; description?: string; parameters?: Params; execute: ( args: z.infer<Params>, context: Context<T>, ) => Promise<string | ContentResult | TextContent | ImageContent>; }; type ResourceResult = | { text: string; } | { blob: string; }; type InputResourceTemplateArgument = Readonly<{ name: string; description?: string; complete?: ArgumentValueCompleter; }>; type ResourceTemplateArgument = Readonly<{ name: string; description?: string; complete?: ArgumentValueCompleter; }>; type ResourceTemplate< Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], > = { uriTemplate: string; name: string; description?: string; mimeType?: string; arguments: Arguments; complete?: (name: string, value: string) => Promise<Completion>; load: ( args: ResourceTemplateArgumentsToObject<Arguments>, ) => Promise<ResourceResult>; }; type ResourceTemplateArgumentsToObject<T extends { name: string }[]> = { [K in T[number]["name"]]: string; }; type InputResourceTemplate< Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], > = { uriTemplate: string; name: string; description?: string; mimeType?: string; arguments: Arguments; load: ( args: ResourceTemplateArgumentsToObject<Arguments>, ) => Promise<ResourceResult>; }; type Resource = { uri: string; name: string; description?: string; mimeType?: string; load: () => Promise<ResourceResult | ResourceResult[]>; complete?: (name: string, value: string) => Promise<Completion>; }; type ArgumentValueCompleter = (value: string) => Promise<Completion>; type InputPromptArgument = Readonly<{ name: string; description?: string; required?: boolean; complete?: ArgumentValueCompleter; enum?: string[]; }>; type PromptArgumentsToObject<T extends { name: string; required?: boolean }[]> = { [K in T[number]["name"]]: Extract< T[number], { name: K } >["required"] extends true ? string : string | undefined; }; type InputPrompt< Arguments extends InputPromptArgument[] = InputPromptArgument[], Args = PromptArgumentsToObject<Arguments>, > = { name: string; description?: string; arguments?: InputPromptArgument[]; load: (args: Args) => Promise<string>; }; type PromptArgument = Readonly<{ name: string; description?: string; required?: boolean; complete?: ArgumentValueCompleter; enum?: string[]; }>; type Prompt< Arguments extends PromptArgument[] = PromptArgument[], Args = PromptArgumentsToObject<Arguments>, > = { arguments?: PromptArgument[]; complete?: (name: string, value: string) => Promise<Completion>; description?: string; load: (args: Args) => Promise<string>; name: string; }; type ServerOptions<T extends FastMCPSessionAuth> = { name: string; version: `${number}.${number}.${number}`; authenticate?: Authenticate<T>; }; type LoggingLevel = | "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency"; const FastMCPSessionEventEmitterBase: { new (): StrictEventEmitter<EventEmitter, FastMCPSessionEvents>; } = EventEmitter; class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} type SamplingResponse = { model: string; stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; role: "user" | "assistant"; content: TextContent | ImageContent; }; type FastMCPSessionAuth = Record<string, unknown> | undefined; export class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends FastMCPSessionEventEmitter { #capabilities: ServerCapabilities = {}; #clientCapabilities?: ClientCapabilities; #loggingLevel: LoggingLevel = "info"; #prompts: Prompt[] = []; #resources: Resource[] = []; #resourceTemplates: ResourceTemplate[] = []; #roots: Root[] = []; #server: Server; #auth: T | undefined; constructor({ auth, name, version, tools, resources, resourcesTemplates, prompts, }: { auth?: T; name: string; version: string; tools: Tool<T>[]; resources: Resource[]; resourcesTemplates: InputResourceTemplate[]; prompts: Prompt[]; }) { super(); this.#auth = auth; if (tools.length) { this.#capabilities.tools = {}; } if (resources.length || resourcesTemplates.length) { this.#capabilities.resources = {}; } if (prompts.length) { for (const prompt of prompts) { this.addPrompt(prompt); } this.#capabilities.prompts = {}; } this.#capabilities.logging = {}; this.#server = new Server( { name: name, version: version }, { capabilities: this.#capabilities }, ); this.setupErrorHandling(); this.setupLoggingHandlers(); this.setupRootsHandlers(); this.setupCompleteHandlers(); if (tools.length) { this.setupToolHandlers(tools); } if (resources.length || resourcesTemplates.length) { for (const resource of resources) { this.addResource(resource); } this.setupResourceHandlers(resources); if (resourcesTemplates.length) { for (const resourceTemplate of resourcesTemplates) { this.addResourceTemplate(resourceTemplate); } this.setupResourceTemplateHandlers(resourcesTemplates); } } if (prompts.length) { this.setupPromptHandlers(prompts); } } private addResource(inputResource: Resource) { this.#resources.push(inputResource); } private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) { const completers: Record<string, ArgumentValueCompleter> = {}; for (const argument of inputResourceTemplate.arguments ?? []) { if (argument.complete) { completers[argument.name] = argument.complete; } } const resourceTemplate = { ...inputResourceTemplate, complete: async (name: string, value: string) => { if (completers[name]) { return await completers[name](value); } return { values: [], }; }, }; this.#resourceTemplates.push(resourceTemplate); } private addPrompt(inputPrompt: InputPrompt) { const completers: Record<string, ArgumentValueCompleter> = {}; const enums: Record<string, string[]> = {}; for (const argument of inputPrompt.arguments ?? []) { if (argument.complete) { completers[argument.name] = argument.complete; } if (argument.enum) { enums[argument.name] = argument.enum; } } const prompt = { ...inputPrompt, complete: async (name: string, value: string) => { if (completers[name]) { return await completers[name](value); } if (enums[name]) { const fuse = new Fuse(enums[name], { keys: ["value"], }); const result = fuse.search(value); return { values: result.map((item) => item.item), total: result.length, }; } return { values: [], }; }, }; this.#prompts.push(prompt); } public get clientCapabilities(): ClientCapabilities | null { return this.#clientCapabilities ?? null; } public get server(): Server { return this.#server; } #pingInterval: ReturnType<typeof setInterval> | null = null; public async requestSampling( message: z.infer<typeof CreateMessageRequestSchema>["params"], ): Promise<SamplingResponse> { return this.#server.createMessage(message); } public async connect(transport: Transport) { if (this.#server.transport) { throw new UnexpectedStateError("Server is already connected"); } await this.#server.connect(transport); let attempt = 0; while (attempt++ < 10) { const capabilities = await this.#server.getClientCapabilities(); if (capabilities) { this.#clientCapabilities = capabilities; break; } await delay(100); } if (!this.#clientCapabilities) { console.warn('[warning] FastMCP could not infer client capabilities') } if (this.#clientCapabilities?.roots?.listChanged) { try { const roots = await this.#server.listRoots(); this.#roots = roots.roots; } catch(e) { console.error(`[error] FastMCP received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`) } } this.#pingInterval = setInterval(async () => { try { await this.#server.ping(); } catch (error) { this.emit("error", { error: error as Error, }); } }, 1000); } public get roots(): Root[] { return this.#roots; } public async close() { if (this.#pingInterval) { clearInterval(this.#pingInterval); } try { await this.#server.close(); } catch (error) { console.error("[MCP Error]", "could not close server", error); } } private setupErrorHandling() { this.#server.onerror = (error) => { console.error("[MCP Error]", error); }; } public get loggingLevel(): LoggingLevel { return this.#loggingLevel; } private setupCompleteHandlers() { this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { if (request.params.ref.type === "ref/prompt") { const prompt = this.#prompts.find( (prompt) => prompt.name === request.params.ref.name, ); if (!prompt) { throw new UnexpectedStateError("Unknown prompt", { request, }); } if (!prompt.complete) { throw new UnexpectedStateError("Prompt does not support completion", { request, }); } const completion = CompletionZodSchema.parse( await prompt.complete( request.params.argument.name, request.params.argument.value, ), ); return { completion, }; } if (request.params.ref.type === "ref/resource") { const resource = this.#resourceTemplates.find( (resource) => resource.uriTemplate === request.params.ref.uri, ); if (!resource) { throw new UnexpectedStateError("Unknown resource", { request, }); } if (!("uriTemplate" in resource)) { throw new UnexpectedStateError("Unexpected resource"); } if (!resource.complete) { throw new UnexpectedStateError( "Resource does not support completion", { request, }, ); } const completion = CompletionZodSchema.parse( await resource.complete( request.params.argument.name, request.params.argument.value, ), ); return { completion, }; } throw new UnexpectedStateError("Unexpected completion request", { request, }); }); } private setupRootsHandlers() { this.#server.setNotificationHandler( RootsListChangedNotificationSchema, () => { this.#server.listRoots().then((roots) => { this.#roots = roots.roots; this.emit("rootsChanged", { roots: roots.roots, }); }); }, ); } private setupLoggingHandlers() { this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { this.#loggingLevel = request.params.level; return {}; }); } private setupToolHandlers(tools: Tool<T>[]) { this.#server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: tools.map((tool) => { return { name: tool.name, description: tool.description, inputSchema: tool.parameters ? zodToJsonSchema(tool.parameters) : undefined, }; }), }; }); this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { const tool = tools.find((tool) => tool.name === request.params.name); if (!tool) { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`, ); } let args: any = undefined; if (tool.parameters) { const parsed = tool.parameters.safeParse(request.params.arguments); if (!parsed.success) { throw new McpError( ErrorCode.InvalidParams, `Invalid ${request.params.name} parameters`, ); } args = parsed.data; } const progressToken = request.params?._meta?.progressToken; let result: ContentResult; try { const reportProgress = async (progress: Progress) => { await this.#server.notification({ method: "notifications/progress", params: { ...progress, progressToken, }, }); }; const log = { debug: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ level: "debug", data: { message, context, }, }); }, error: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ level: "error", data: { message, context, }, }); }, info: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ level: "info", data: { message, context, }, }); }, warn: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ level: "warning", data: { message, context, }, }); }, }; const maybeStringResult = await tool.execute(args, { reportProgress, log, session: this.#auth, }); if (typeof maybeStringResult === "string") { result = ContentResultZodSchema.parse({ content: [{ type: "text", text: maybeStringResult }], }); } else if ("type" in maybeStringResult) { result = ContentResultZodSchema.parse({ content: [maybeStringResult], }); } else { result = ContentResultZodSchema.parse(maybeStringResult); } } catch (error) { if (error instanceof UserError) { return { content: [{ type: "text", text: error.message }], isError: true, }; } return { content: [{ type: "text", text: `Error: ${error}` }], isError: true, }; } return result; }); } private setupResourceHandlers(resources: Resource[]) { this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: resources.map((resource) => { return { uri: resource.uri, name: resource.name, mimeType: resource.mimeType, }; }), }; }); this.#server.setRequestHandler( ReadResourceRequestSchema, async (request) => { if ("uri" in request.params) { const resource = resources.find( (resource) => "uri" in resource && resource.uri === request.params.uri, ); if (!resource) { for (const resourceTemplate of this.#resourceTemplates) { const uriTemplate = parseURITemplate( resourceTemplate.uriTemplate, ); const match = uriTemplate.fromUri(request.params.uri); if (!match) { continue; } const uri = uriTemplate.fill(match); const result = await resourceTemplate.load(match); return { contents: [ { uri: uri, mimeType: resourceTemplate.mimeType, name: resourceTemplate.name, ...result, }, ], }; } throw new McpError( ErrorCode.MethodNotFound, `Unknown resource: ${request.params.uri}`, ); } if (!("uri" in resource)) { throw new UnexpectedStateError("Resource does not support reading"); } let maybeArrayResult: Awaited<ReturnType<Resource["load"]>>; try { maybeArrayResult = await resource.load(); } catch (error) { throw new McpError( ErrorCode.InternalError, `Error reading resource: ${error}`, { uri: resource.uri, }, ); } if (Array.isArray(maybeArrayResult)) { return { contents: maybeArrayResult.map((result) => ({ uri: resource.uri, mimeType: resource.mimeType, name: resource.name, ...result, })), }; } else { return { contents: [ { uri: resource.uri, mimeType: resource.mimeType, name: resource.name, ...maybeArrayResult, }, ], }; } } throw new UnexpectedStateError("Unknown resource request", { request, }); }, ); } private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) { this.#server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: resourceTemplates.map((resourceTemplate) => { return { name: resourceTemplate.name, uriTemplate: resourceTemplate.uriTemplate, }; }), }; }, ); } private setupPromptHandlers(prompts: Prompt[]) { this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: prompts.map((prompt) => { return { name: prompt.name, description: prompt.description, arguments: prompt.arguments, complete: prompt.complete, }; }), }; }); this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { const prompt = prompts.find( (prompt) => prompt.name === request.params.name, ); if (!prompt) { throw new McpError( ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`, ); } const args = request.params.arguments; for (const arg of prompt.arguments ?? []) { if (arg.required && !(args && arg.name in args)) { throw new McpError( ErrorCode.InvalidRequest, `Missing required argument: ${arg.name}`, ); } } let result: Awaited<ReturnType<Prompt["load"]>>; try { result = await prompt.load(args as Record<string, string | undefined>); } catch (error) { throw new McpError( ErrorCode.InternalError, `Error loading prompt: ${error}`, ); } return { description: prompt.description, messages: [ { role: "user", content: { type: "text", text: result }, }, ], }; }); } } const FastMCPEventEmitterBase: { new (): StrictEventEmitter<EventEmitter, FastMCPEvents<FastMCPSessionAuth>>; } = EventEmitter; class FastMCPEventEmitter extends FastMCPEventEmitterBase {} type Authenticate<T> = (request: http.IncomingMessage) => Promise<T>; export class FastMCP<T extends Record<string, unknown> | undefined = undefined> extends FastMCPEventEmitter { #options: ServerOptions<T>; #prompts: InputPrompt[] = []; #resources: Resource[] = []; #resourcesTemplates: InputResourceTemplate[] = []; #sessions: FastMCPSession<T>[] = []; #sseServer: SSEServer | null = null; #tools: Tool<T>[] = []; #authenticate: Authenticate<T> | undefined; constructor(public options: ServerOptions<T>) { super(); this.#options = options; this.#authenticate = options.authenticate; } public get sessions(): FastMCPSession<T>[] { return this.#sessions; } /** * Adds a tool to the server. */ public addTool<Params extends ToolParameters>(tool: Tool<T, Params>) { this.#tools.push(tool as unknown as Tool<T>); } /** * Adds a resource to the server. */ public addResource(resource: Resource) { this.#resources.push(resource); } /** * Adds a resource template to the server. */ public addResourceTemplate< const Args extends InputResourceTemplateArgument[], >(resource: InputResourceTemplate<Args>) { this.#resourcesTemplates.push(resource); } /** * Adds a prompt to the server. */ public addPrompt<const Args extends InputPromptArgument[]>( prompt: InputPrompt<Args>, ) { this.#prompts.push(prompt); } /** * Starts the server. */ public async start( options: | { transportType: "stdio" } | { transportType: "sse"; sse: { endpoint: `/${string}`; port: number }; } = { transportType: "stdio", }, ) { if (options.transportType === "stdio") { const transport = new StdioServerTransport(); const session = new FastMCPSession<T>({ name: this.#options.name, version: this.#options.version, tools: this.#tools, resources: this.#resources, resourcesTemplates: this.#resourcesTemplates, prompts: this.#prompts, }); await session.connect(transport); this.#sessions.push(session); this.emit("connect", { session, }); } else if (options.transportType === "sse") { this.#sseServer = await startSSEServer<FastMCPSession<T>>({ endpoint: options.sse.endpoint as `/${string}`, port: options.sse.port, createServer: async (request) => { let auth: T | undefined; if (this.#authenticate) { auth = await this.#authenticate(request); } return new FastMCPSession<T>({ auth, name: this.#options.name, version: this.#options.version, tools: this.#tools, resources: this.#resources, resourcesTemplates: this.#resourcesTemplates, prompts: this.#prompts, }); }, onClose: (session) => { this.emit("disconnect", { session, }); }, onConnect: async (session) => { this.#sessions.push(session); this.emit("connect", { session, }); }, }); console.info( `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`, ); } else { throw new Error("Invalid transport type"); } } /** * Stops the server. */ public async stop() { if (this.#sseServer) { this.#sseServer.close(); } } } export type { Context }; export type { Tool, ToolParameters }; export type { Content, TextContent, ImageContent, ContentResult }; export type { Progress, SerializableValue }; export type { Resource, ResourceResult }; export type { ResourceTemplate, ResourceTemplateArgument }; export type { Prompt, PromptArgument }; export type { InputPrompt, InputPromptArgument }; export type { ServerOptions, LoggingLevel }; export type { FastMCPEvents, FastMCPSessionEvents }; --- File: /eslint.config.js --- import perfectionist from "eslint-plugin-perfectionist"; export default [perfectionist.configs["recommended-alphabetical"]]; --- File: /package.json --- { "name": "fastmcp", "version": "1.0.0", "main": "dist/FastMCP.js", "scripts": { "build": "tsup", "test": "vitest run && tsc && jsr publish --dry-run", "format": "prettier --write . && eslint --fix ." }, "bin": { "fastmcp": "dist/bin/fastmcp.js" }, "keywords": [ "MCP", "SSE" ], "type": "module", "author": "Frank Fiegel <[email protected]>", "license": "MIT", "description": "A TypeScript framework for building MCP servers.", "module": "dist/FastMCP.js", "types": "dist/FastMCP.d.ts", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.0", "execa": "^9.5.2", "file-type": "^20.3.0", "fuse.js": "^7.1.0", "mcp-proxy": "^2.10.4", "strict-event-emitter-types": "^2.0.0", "undici": "^7.4.0", "uri-templates": "^0.2.0", "yargs": "^17.7.2", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.3" }, "repository": { "url": "https://github.com/punkpeye/fastmcp" }, "homepage": "https://glama.ai/mcp", "release": { "branches": [ "main" ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github", "@sebbo2002/semantic-release-jsr" ] }, "devDependencies": { "@sebbo2002/semantic-release-jsr": "^2.0.4", "@tsconfig/node22": "^22.0.0", "@types/node": "^22.13.5", "@types/uri-templates": "^0.1.34", "@types/yargs": "^17.0.33", "eslint": "^9.21.0", "eslint-plugin-perfectionist": "^4.9.0", "eventsource-client": "^1.1.3", "get-port-please": "^3.1.2", "jsr": "^0.13.3", "prettier": "^3.5.2", "semantic-release": "^24.2.3", "tsup": "^8.4.0", "typescript": "^5.7.3", "vitest": "^3.0.7" }, "tsup": { "entry": [ "src/FastMCP.ts", "src/bin/fastmcp.ts" ], "format": [ "esm" ], "dts": true, "splitting": true, "sourcemap": true, "clean": true } } --- File: /README.md --- # FastMCP A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable of handling client sessions. > [!NOTE] > > For a Python implementation, see [FastMCP](https://github.com/jlowin/fastmcp). ## Features - Simple Tool, Resource, Prompt definition - [Authentication](#authentication) - [Sessions](#sessions) - [Image content](#returning-an-image) - [Logging](#logging) - [Error handling](#errors) - [SSE](#sse) - CORS (enabled by default) - [Progress notifications](#progress) - [Typed server events](#typed-server-events) - [Prompt argument auto-completion](#prompt-argument-auto-completion) - [Sampling](#requestsampling) - Automated SSE pings - Roots - CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector) ## Installation ```bash npm install fastmcp ``` ## Quickstart ```ts import { FastMCP } from "fastmcp"; import { z } from "zod"; const server = new FastMCP({ name: "My Server", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); server.start({ transportType: "stdio", }); ``` _That's it!_ You have a working MCP server. You can test the server in terminal with: ```bash git clone https://github.com/punkpeye/fastmcp.git cd fastmcp npm install # Test the addition server example using CLI: npx fastmcp dev src/examples/addition.ts # Test the addition server example using MCP Inspector: npx fastmcp inspect src/examples/addition.ts ``` ### SSE You can also run the server with SSE support: ```ts server.start({ transportType: "sse", sse: { endpoint: "/sse", port: 8080, }, }); ``` This will start the server and listen for SSE connections on `http://localhost:8080/sse`. You can then use `SSEClientTransport` to connect to the server: ```ts import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)); await client.connect(transport); ``` ## Core Concepts ### Tools [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. ```js server.addTool({ name: "fetch", description: "Fetch the content of a url", parameters: z.object({ url: z.string(), }), execute: async (args) => { return await fetchWebpageContent(args.url); }, }); ``` #### Returning a string `execute` can return a string: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return "Hello, world!"; }, }); ``` The latter is equivalent to: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "text", text: "Hello, world!", }, ], }; }, }); ``` #### Returning a list If you want to return a list of messages, you can return an object with a `content` property: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "text", text: "First message" }, { type: "text", text: "Second message" }, ], }; }, }); ``` #### Returning an image Use the `imageContent` to create a content object for an image: ```js import { imageContent } from "fastmcp"; server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return imageContent({ url: "https://example.com/image.png", }); // or... // return imageContent({ // path: "/path/to/image.png", // }); // or... // return imageContent({ // buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"), // }); // or... // return { // content: [ // await imageContent(...) // ], // }; }, }); ``` The `imageContent` function takes the following options: - `url`: The URL of the image. - `path`: The path to the image file. - `buffer`: The image data as a buffer. Only one of `url`, `path`, or `buffer` must be specified. The above example is equivalent to: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "image", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "image/png", }, ], }; }, }); ``` #### Logging Tools can log messages to the client using the `log` object in the context object: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args, { log }) => { log.info("Downloading file...", { url, }); // ... log.info("Downloaded file"); return "done"; }, }); ``` The `log` object has the following methods: - `debug(message: string, data?: SerializableValue)` - `error(message: string, data?: SerializableValue)` - `info(message: string, data?: SerializableValue)` - `warn(message: string, data?: SerializableValue)` #### Errors The errors that are meant to be shown to the user should be thrown as `UserError` instances: ```js import { UserError } from "fastmcp"; server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { if (args.url.startsWith("https://example.com")) { throw new UserError("This URL is not allowed"); } return "done"; }, }); ``` #### Progress Tools can report progress by calling `reportProgress` in the context object: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args, { reportProgress }) => { reportProgress({ progress: 0, total: 100, }); // ... reportProgress({ progress: 100, total: 100, }); return "done"; }, }); ``` ### Resources [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: - File contents - Screenshots and images - Log files - And more Each resource is identified by a unique URI and can contain either text or binary data. ```ts server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: await readLogFile(), }; }, }); ``` > [!NOTE] > > `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. > > ```ts > async load() { > return [ > { > text: "First file content", > }, > { > text: "Second file content", > }, > ]; > } > ``` You can also return binary contents in `load`: ```ts async load() { return { blob: 'base64-encoded-data' }; } ``` ### Resource templates You can also define resource templates: ```ts server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", required: true, }, ], async load({ name }) { return { text: `Example log content for ${name}`, }; }, }); ``` #### Resource template argument auto-completion Provide `complete` functions for resource template arguments to enable automatic completion: ```ts server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", required: true, complete: async (value) => { if (value === "Example") { return { values: ["Example Log"], }; } return { values: [], }; }, }, ], async load({ name }) { return { text: `Example log content for ${name}`, }; }, }); ``` ### Prompts [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. ```ts server.addPrompt({ name: "git-commit", description: "Generate a Git commit message", arguments: [ { name: "changes", description: "Git diff or description of changes", required: true, }, ], load: async (args) => { return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, }); ``` #### Prompt argument auto-completion Prompts can provide auto-completion for their arguments: ```js server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, arguments: [ { name: "name", description: "Name of the country", required: true, complete: async (value) => { if (value === "Germ") { return { values: ["Germany"], }; } return { values: [], }; }, }, ], }); ``` #### Prompt argument auto-completion using `enum` If you provide an `enum` array for an argument, the server will automatically provide completions for the argument. ```js server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, arguments: [ { name: "name", description: "Name of the country", required: true, enum: ["Germany", "France", "Italy"], }, ], }); ``` ### Authentication FastMCP allows you to `authenticate` clients using a custom function: ```ts import { AuthError } from "fastmcp"; const server = new FastMCP({ name: "My Server", version: "1.0.0", authenticate: ({request}) => { const apiKey = request.headers["x-api-key"]; if (apiKey !== '123') { throw new Response(null, { status: 401, statusText: "Unauthorized", }); } // Whatever you return here will be accessible in the `context.session` object. return { id: 1, } }, }); ``` Now you can access the authenticated session data in your tools: ```ts server.addTool({ name: "sayHello", execute: async (args, { session }) => { return `Hello, ${session.id}!`; }, }); ``` ### Sessions The `session` object is an instance of `FastMCPSession` and it describes active client sessions. ```ts server.sessions; ``` We allocate a new server instance for each client connection to enable 1:1 communication between a client and the server. ### Typed server events You can listen to events emitted by the server using the `on` method: ```ts server.on("connect", (event) => { console.log("Client connected:", event.session); }); server.on("disconnect", (event) => { console.log("Client disconnected:", event.session); }); ``` ## `FastMCPSession` `FastMCPSession` represents a client session and provides methods to interact with the client. Refer to [Sessions](#sessions) for examples of how to obtain a `FastMCPSession` instance. ### `requestSampling` `requestSampling` creates a [sampling](https://modelcontextprotocol.io/docs/concepts/sampling) request and returns the response. ```ts await session.requestSampling({ messages: [ { role: "user", content: { type: "text", text: "What files are in the current directory?", }, }, ], systemPrompt: "You are a helpful file system assistant.", includeContext: "thisServer", maxTokens: 100, }); ``` ### `clientCapabilities` The `clientCapabilities` property contains the client capabilities. ```ts session.clientCapabilities; ``` ### `loggingLevel` The `loggingLevel` property describes the logging level as set by the client. ```ts session.loggingLevel; ``` ### `roots` The `roots` property contains the roots as set by the client. ```ts session.roots; ``` ### `server` The `server` property contains an instance of MCP server that is associated with the session. ```ts session.server; ``` ### Typed session events You can listen to events emitted by the session using the `on` method: ```ts session.on("rootsChanged", (event) => { console.log("Roots changed:", event.roots); }); session.on("error", (event) => { console.error("Error:", event.error); }); ``` ## Running Your Server ### Test with `mcp-cli` The fastest way to test and debug your server is with `fastmcp dev`: ```bash npx fastmcp dev server.js npx fastmcp dev server.ts ``` This will run your server with [`mcp-cli`](https://github.com/wong2/mcp-cli) for testing and debugging your MCP server in the terminal. ### Inspect with `MCP Inspector` Another way is to use the official [`MCP Inspector`](https://modelcontextprotocol.io/docs/tools/inspector) to inspect your server with a Web UI: ```bash npx fastmcp inspect server.ts ``` ## FAQ ### How to use with Claude Desktop? Follow the guide https://modelcontextprotocol.io/quickstart/user and add the following configuration: ```json { "mcpServers": { "my-mcp-server": { "command": "npx", "args": [ "tsx", "/PATH/TO/YOUR_PROJECT/src/index.ts" ], "env": { "YOUR_ENV_VAR": "value" } } } } ``` ## Showcase > [!NOTE] > > If you've developed a server using FastMCP, please [submit a PR](https://github.com/punkpeye/fastmcp) to showcase it here! - https://github.com/apinetwork/piapi-mcp-server - 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 ## Acknowledgements - FastMCP is inspired by the [Python implementation](https://github.com/jlowin/fastmcp) by [Jonathan Lowin](https://github.com/jlowin). - Parts of codebase were adopted from [LiteMCP](https://github.com/wong2/litemcp). - Parts of codebase were adopted from [Model Context protocolでSSEをやってみる](https://dev.classmethod.jp/articles/mcp-sse/). --- File: /vitest.config.js --- import { defineConfig } from "vitest/config"; export default defineConfig({ test: { poolOptions: { forks: { execArgv: ["--experimental-eventsource"] }, }, }, }); ```