diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3e2e84b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +build/ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..446a091 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "extends": ["eslint:recommended"], + "rules": {} +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e57a3d3..7493add 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,8 +92,8 @@ jobs: fi # Verify build succeeded (postgenerate hook builds automatically) - if [ ! -f "build/build/index.js" ]; then - echo "โŒ Build failed - build/build/index.js not found" + if [ ! -f "build/dist/index.js" ]; then + echo "โŒ Build failed - build/dist/index.js not found" exit 1 fi diff --git a/.github/workflows/generate-windmill-version.yml b/.github/workflows/generate-windmill-version.yml index cc41446..21fad19 100644 --- a/.github/workflows/generate-windmill-version.yml +++ b/.github/workflows/generate-windmill-version.yml @@ -48,13 +48,13 @@ jobs: echo "๐Ÿ“ฅ Fetching OpenAPI spec from running Windmill instance..." # Ensure cache directory exists - mkdir -p generator/cache + mkdir -p cache # Fetch from local Windmill instance - curl -f http://localhost:8000/api/openapi.json -o generator/cache/openapi-spec.json + curl -f http://localhost:8000/api/openapi.json -o cache/openapi-spec.json # Extract version from spec - SPEC_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('generator/cache/openapi-spec.json', 'utf8')).info.version") + SPEC_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('cache/openapi-spec.json', 'utf8')).info.version") echo "spec_version=$SPEC_VERSION" >> $GITHUB_OUTPUT echo "โœ… Fetched OpenAPI spec version: $SPEC_VERSION" @@ -71,8 +71,8 @@ jobs: fi # Verify build succeeded (postgenerate hook builds automatically) - if [ ! -f "build/build/index.js" ]; then - echo "โŒ Build failed - build/build/index.js not found" + if [ ! -f "build/dist/index.js" ]; then + echo "โŒ Build failed - build/dist/index.js not found" exit 1 fi @@ -218,7 +218,7 @@ jobs: Generated by workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} files: | generated-mcp-server.tar.gz - generator/cache/openapi-spec.json + cache/openapi-spec.json draft: false prerelease: false make_latest: ${{ github.event.inputs.windmill_version == 'latest' || github.event_name == 'schedule' }} diff --git a/.github/workflows/rebuild-all-versions.yml b/.github/workflows/rebuild-all-versions.yml index 70d7227..eaa0868 100644 --- a/.github/workflows/rebuild-all-versions.yml +++ b/.github/workflows/rebuild-all-versions.yml @@ -85,14 +85,14 @@ jobs: echo "๐Ÿ“ฅ Fetching OpenAPI spec for Windmill ${{ matrix.version }}..." # Ensure cache directory exists - mkdir -p generator/cache + mkdir -p cache # Fetch from local Windmill instance # Note: In reality, we'd need a way to fetch specific version specs # For now, we use the running instance - curl -f http://localhost:8000/api/openapi.json -o generator/cache/openapi-spec.json + curl -f http://localhost:8000/api/openapi.json -o cache/openapi-spec.json - SPEC_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('generator/cache/openapi-spec.json', 'utf8')).info.version") + SPEC_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('cache/openapi-spec.json', 'utf8')).info.version") echo "spec_version=$SPEC_VERSION" >> $GITHUB_OUTPUT echo "โœ… Fetched OpenAPI spec version: $SPEC_VERSION" @@ -108,8 +108,8 @@ jobs: fi # Verify build succeeded (postgenerate hook builds automatically) - if [ ! -f "build/build/index.js" ]; then - echo "โŒ Build failed - build/build/index.js not found" + if [ ! -f "build/dist/index.js" ]; then + echo "โŒ Build failed - build/dist/index.js not found" exit 1 fi @@ -127,7 +127,7 @@ jobs: env: E2E_WINDMILL_URL: http://localhost:8000 E2E_WINDMILL_TOKEN: test-super-secret - E2E_WORKSPACE: demo + E2E_WORKSPACE: admins WINDMILL_BASE_URL: http://localhost:8000 run: | echo "๐Ÿงช Running E2E tests..." @@ -195,7 +195,7 @@ jobs: Generated by workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} files: | generated-mcp-server.tar.gz - generator/cache/openapi-spec.json + cache/openapi-spec.json draft: false prerelease: false diff --git a/.ls-lint.yml b/.ls-lint.yml index 84486f1..07c3a95 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -1,13 +1,17 @@ ls: # Root directory - specific uppercase files allowed .js: regex:^vitest\.config$ - .json: regex:^package$ + .json: regex:^(package|\.eslintrc)$ .yml: regex:^\.ls-lint$ .md: regex:^(README|CHANGELOG|CONTRIBUTING|PROJECT_STATUS|AGENTS)$ .example: regex:^\.env$ - .ignore: regex:^(\.gitignore|\.npmignore)$ + .ignore: regex:^(\.gitignore|\.npmignore|\.eslintignore)$ .dir: regex:^([a-z][a-z0-9-]*|\.github|\.husky)$ # kebab-case or conventional dot-directories + # Scripts directory + scripts: + .js: kebab-case + # GitHub workflows .github/workflows: .yml: kebab-case @@ -48,3 +52,4 @@ ignore: - .env.local - package-lock.json - npm-debug.log + - .opencode # OpenCode configuration (user-specific) diff --git a/AGENTS.md b/AGENTS.md index e589bf2..abb1ae2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,10 +23,10 @@ This project uses AI agents to assist with development, maintenance, and testing **Key Files**: -- `generator/config.json` -- `generator/fetch-spec.js` -- `generator/generate.js` -- `overrides/apply-overrides.js` +- `src/generator/config.json` +- `src/generator/fetch-spec.js` +- `src/generator/generate.js` +- `src/overrides/apply-overrides.js` **Commands**: @@ -48,7 +48,7 @@ The `npm run generate` command executes a complete workflow: 3. **Post-generation** (`postgenerate` hook): - Applies custom overrides from `overrides/` - Installs dependencies in `build/` - - Compiles TypeScript to `build/build/index.js` + - Compiles TypeScript to `build/dist/index.js` **Output Structure**: @@ -64,7 +64,7 @@ build/ **Troubleshooting**: -- If build fails, check `build/build/index.js` exists after generation +- If build fails, check `build/dist/index.js` exists after generation - Generated code location changed from `src/` to `build/` in recent updates - The complete workflow is now atomic - no need to manually build after generation @@ -286,9 +286,9 @@ RUN_TESTS_ON_GENERATE=true Store in respective config files: -- Generator: `generator/config.json` +- Generator: `src/generator/config.json` - Testing: `tests/config.json` -- Overrides: `overrides/config.json` +- Overrides: `src/overrides/config.json` --- diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index d11d371..f5c6c47 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,7 +1,7 @@ # Project Status -**Last Updated**: 2025-11-14 -**Status**: โœ… Foundation Complete - Ready for Phase 2 (with Automated Updates) +**Last Updated**: 2025-11-18 +**Status**: โœ… Agent Team Complete - Full Tooling & Specialization Ready ## Overview @@ -16,18 +16,24 @@ The Windmill MCP Server generator project foundation is complete with: - โœ… npm package configuration (npx execution ready) - โœ… All unit tests passing (13/13) - โœ… Automated GitHub Actions workflow for updates +- โœ… Complete agent team configuration (13 agents) +- โœ… Tool namespacing system (501 tools โ†’ 59 categories) +- โœ… Auto-generated tool documentation ## Quick Stats | Metric | Count | | ---------------------- | ---------------- | -| Total Files | 26 | -| Documentation Files | 10 | -| Scripts | 7 | +| Total Files | 45 | +| Documentation Files | 13 | +| Scripts | 9 | | Test Files | 4 | | Config Files | 5 | +| Agent Configs | 13 | +| MCP Tools Generated | 501 | +| Tool Categories | 59 | | Unit Tests | 13 passing โœ… | -| Lines of Documentation | ~2000+ | +| Lines of Documentation | ~3500+ | | Dependencies | 3 runtime, 5 dev | ## Project Structure @@ -39,15 +45,35 @@ windmill-mcp/ โ”‚ โ”‚ โ””โ”€โ”€ AGENTS.md # Agent roles and workflows โ”‚ โ””โ”€โ”€ workflows/ # GitHub Actions workflows โ”‚ โ””โ”€โ”€ update-mcp-server.yml # Automated update workflow +โ”œโ”€โ”€ .opencode/ +โ”‚ โ”œโ”€โ”€ agent/ # Agent configurations +โ”‚ โ”‚ โ”œโ”€โ”€ windmill-manager.md # Primary coordinator +โ”‚ โ”‚ โ”œโ”€โ”€ job-specialist.md # Job operations +โ”‚ โ”‚ โ”œโ”€โ”€ user-specialist.md # User management +โ”‚ โ”‚ โ”œโ”€โ”€ script-specialist.md # Script operations +โ”‚ โ”‚ โ”œโ”€โ”€ flow-specialist.md # Flow orchestration +โ”‚ โ”‚ โ”œโ”€โ”€ resource-specialist.md # Resource management +โ”‚ โ”‚ โ”œโ”€โ”€ trigger-specialist.md # Triggers & schedules +โ”‚ โ”‚ โ”œโ”€โ”€ app-specialist.md # Application management +โ”‚ โ”‚ โ”œโ”€โ”€ workspace-specialist.md # Workspaces & folders +โ”‚ โ”‚ โ”œโ”€โ”€ audit-specialist.md # Audit logs & security +โ”‚ โ”‚ โ”œโ”€โ”€ integration-specialist.md # OAuth, webhooks, integrations +โ”‚ โ”‚ โ”œโ”€โ”€ storage-specialist.md # Database & file storage +โ”‚ โ”‚ โ””โ”€โ”€ system-specialist.md # System health & configuration +โ”‚ โ””โ”€โ”€ templates/ +โ”‚ โ””โ”€โ”€ agent/ +โ”‚ โ””โ”€โ”€ windmill-ROLE.md # Agent template โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ generator/ # OpenAPI spec fetching & generation -โ”‚ โ”‚ โ”œโ”€โ”€ config.json # Generator configuration -โ”‚ โ”‚ โ”œโ”€โ”€ fetch-spec.js # Fetch OpenAPI specs -โ”‚ โ”‚ โ””โ”€โ”€ generate.js # Generate MCP server +โ”‚ โ”‚ โ”œโ”€โ”€ config.json # Generator configuration +โ”‚ โ”‚ โ”œโ”€โ”€ fetch-spec.js # Fetch OpenAPI specs +โ”‚ โ”‚ โ”œโ”€โ”€ generate.js # Generate MCP server +โ”‚ โ”‚ โ””โ”€โ”€ generate-tool-list.js # Generate tool documentation โ”‚ โ”œโ”€โ”€ overrides/ # Custom code overrides -โ”‚ โ”‚ โ”œโ”€โ”€ README.md # Override documentation -โ”‚ โ”‚ โ”œโ”€โ”€ apply-overrides.js # Apply custom overrides -โ”‚ โ”‚ โ””โ”€โ”€ validate-overrides.js # Validate override syntax +โ”‚ โ”‚ โ”œโ”€โ”€ README.md # Override documentation +โ”‚ โ”‚ โ”œโ”€โ”€ apply-overrides.js # Apply custom overrides +โ”‚ โ”‚ โ”œโ”€โ”€ add-tool-namespaces.js # Add namespace prefixes to tools +โ”‚ โ”‚ โ””โ”€โ”€ validate-overrides.js # Validate override syntax โ”‚ โ””โ”€โ”€ runtime/ # Runtime loader for version management โ”‚ โ”œโ”€โ”€ index.js # Main entry point โ”‚ โ”œโ”€โ”€ cache.js # Cache management @@ -65,10 +91,13 @@ windmill-mcp/ โ”‚ โ”œโ”€โ”€ utils/ # Test utilities & mocks โ”‚ โ”œโ”€โ”€ setup.js # Test setup โ”‚ โ””โ”€โ”€ config.json # Test configuration -โ”œโ”€โ”€ docs/ # Documentation -โ”‚ โ”œโ”€โ”€ quickstart.md # Getting started guide -โ”‚ โ”œโ”€โ”€ testing.md # Testing guide -โ”‚ โ””โ”€โ”€ architecture-verification.md +โ”œโ”€โ”€ docs/ # Documentation +โ”‚ โ”œโ”€โ”€ quickstart.md # Getting started guide +โ”‚ โ”œโ”€โ”€ testing.md # Testing guide +โ”‚ โ”œโ”€โ”€ architecture-verification.md +โ”‚ โ”œโ”€โ”€ agent-setup-complete.md # Agent team setup summary +โ”‚ โ”œโ”€โ”€ windmill-agent-team-plan.md # Agent architecture & workflows +โ”‚ โ””โ”€โ”€ generated-tools.md # Auto-generated tool list (501 tools) โ”œโ”€โ”€ README.md # Main documentation โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines โ”œโ”€โ”€ CHANGELOG.md # Version history @@ -150,6 +179,17 @@ windmill-mcp/ - [x] Test result artifacts - [x] Workflow summary reporting +### โœ… Agent Team & Tool Organization + +- [x] Complete agent team architecture (1 manager + 12 specialists) +- [x] Tool namespacing system (namespace_subgroup_operationId format) +- [x] 501 tools organized into 59 categories +- [x] Auto-generated tool documentation +- [x] Agent configurations with YAML frontmatter +- [x] Glob pattern-based tool access control +- [x] Deny-by-default security model +- [x] Agent team workflow documentation + ## Test Results ### Unit Tests โœ… @@ -329,31 +369,34 @@ The project includes an automated workflow for keeping the MCP server up to date - **Rationale**: Standard MCP server pattern, better UX - **Impact**: Easier adoption, simpler for end users -## Next Steps (Phase 2) +## Next Steps (Phase 3) ### Immediate - [x] Set up GitHub Actions CI/CD for automated updates -- [ ] Install openapi-mcp-generator as dependency -- [ ] Integrate actual MCP server generation -- [ ] Create real tool implementations from OpenAPI -- [ ] Add more unit tests for generated code +- [x] Complete agent team configuration (13 agents) +- [x] Implement tool namespacing system +- [x] Generate tool documentation +- [ ] Test agent coordination workflows +- [ ] Validate tool access patterns +- [ ] Create example agent workflows ### Short Term -- [ ] Publish v0.1.0 to npm -- [ ] Create example MCP client integration -- [ ] Add integration tests -- [ ] Implement caching for OpenAPI specs -- [ ] Add more override examples +- [ ] Test MCP server with actual Windmill instance +- [ ] Validate agent tool access in practice +- [ ] Create agent coordination examples +- [ ] Add integration tests for agent workflows +- [ ] Document common agent patterns ### Medium Term -- [ ] Full E2E test coverage +- [ ] Publish v0.1.0 to npm +- [ ] Full E2E test coverage with agents - [ ] Performance optimization - [ ] Error handling improvements +- [ ] Agent workflow tutorials - [ ] Documentation site -- [ ] Video tutorials ## Dependencies @@ -411,18 +454,24 @@ The project includes an automated workflow for keeping the MCP server up to date - **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) - **Project Plan**: See [docs/project-plan.md](docs/project-plan.md) - **Sprints**: See [docs/sprints.md](docs/sprints.md) +- **Agent Setup**: See [docs/agent-setup-complete.md](docs/agent-setup-complete.md) +- **Agent Team Plan**: See [docs/windmill-agent-team-plan.md](docs/windmill-agent-team-plan.md) +- **Tool List**: See [docs/generated-tools.md](docs/generated-tools.md) ## Conclusion -โœ… **Foundation is complete and solid** +โœ… **Agent Team & Tooling Complete** -The project has a robust foundation with: +The project now has: - Complete project structure -- Working generator system +- Working generator system with tool namespacing +- 13 specialized agents (1 manager + 12 specialists) +- 501 tools organized into 59 categories +- Auto-generated tool documentation - Comprehensive testing infrastructure - All unit tests passing - Excellent documentation -- Ready for actual MCP server generation +- Ready for agent coordination testing -**Ready to proceed to Phase 2!** ๐Ÿš€ +**Ready to proceed to Phase 3 - Agent Testing & Coordination!** ๐Ÿš€ diff --git a/README.md b/README.md index ef4a0f2..a8d8be2 100644 --- a/README.md +++ b/README.md @@ -171,17 +171,17 @@ Test the MCP server using the MCP Inspector: # With local Windmill instance (matches OpenCode config) WINDMILL_BASE_URL=http://localhost:8000 \ WINDMILL_API_TOKEN=test-super-secret \ -npx @modelcontextprotocol/inspector node build/build/index.js +npx @modelcontextprotocol/inspector node build/dist/index.js # Using absolute path (for use outside project directory) WINDMILL_BASE_URL=http://localhost:8000 \ WINDMILL_API_TOKEN=test-super-secret \ -npx @modelcontextprotocol/inspector node /Users/nroth/workspace/windmill-mcp/build/build/index.js +npx @modelcontextprotocol/inspector node /Users/nroth/workspace/windmill-mcp/build/dist/index.js # With your own Windmill instance WINDMILL_BASE_URL=https://your-instance.windmill.dev \ WINDMILL_API_TOKEN=your-api-token \ -npx @modelcontextprotocol/inspector node build/build/index.js +npx @modelcontextprotocol/inspector node build/dist/index.js ``` This will open a web interface where you can: @@ -201,7 +201,7 @@ npm run generate # This single command performs the complete workflow: # 1. Pre-generation: Fetches the latest OpenAPI specification # 2. Generation: Creates TypeScript MCP server in build/src/ -# 3. Post-generation: Applies overrides and builds to build/build/ +# 3. Post-generation: Applies overrides and builds to build/dist/ # The compiled JavaScript is immediately ready to use # No manual build step needed! @@ -257,7 +257,7 @@ This single command performs the complete workflow: 3. **Post-generation**: - Applies custom overrides from the `src/overrides/` directory - Installs dependencies in `build/` - - Builds the TypeScript code to JavaScript in `build/build/` + - Builds the TypeScript code to JavaScript in `build/dist/` The compiled server is immediately ready to use - no additional build steps needed! diff --git a/docs/agent-setup-complete.md b/docs/agent-setup-complete.md new file mode 100644 index 0000000..434f9e2 --- /dev/null +++ b/docs/agent-setup-complete.md @@ -0,0 +1,278 @@ +# Agent Setup Complete โœ… + +## Summary + +Successfully set up a complete agent team configuration for the Windmill MCP Server project with tool namespacing and specialization. + +## What Was Accomplished + +### 1. Tool Namespacing System + +- **Created**: `src/generator/generate-tool-list.js` - Extracts and categorizes tools +- **Created**: `src/overrides/add-tool-namespaces.js` - Adds namespace prefixes to tool names +- **Result**: 501 tools organized into 59 namespace:subgroup categories + +**Namespace Format**: `namespace_subgroup_operationId` + +Examples: + +- `job_list_listJobs` +- `user_auth_login` +- `flow_manage_createFlow` +- `workspace_settings_editWorkspaceGitSyncConfig` + +### 2. Generated Documentation + +- **File**: `docs/generated-tools.md` +- Auto-generated from OpenAPI spec +- Lists all 501 tools organized by category +- Updated on every build via `npm run generate` + +### 3. Agent Team Architecture + +- **File**: `docs/windmill-agent-team-plan.md` +- Defines 1 primary manager + 12 specialized subagents +- Documents responsibilities, tool access patterns, and workflows + +### 4. Agent Configurations Created + +All agents use the principle of **"deny by default, permit explicitly"**: + +#### Primary Manager + +- `.opencode/agent/windmill-manager.md` - Coordinates all operations, delegates to specialists + +#### Subagent Specialists (12 total) + +1. **job-specialist.md** - Job execution & monitoring (`windmill-dev_job_*`) +2. **user-specialist.md** - User management (`windmill-dev_user_*`) +3. **script-specialist.md** - Script operations (`windmill-dev_script_*`) +4. **flow-specialist.md** - Flow orchestration (`windmill-dev_flow_*`) +5. **resource-specialist.md** - Resource management (`windmill-dev_resource_*`) +6. **trigger-specialist.md** - Triggers & schedules (`windmill-dev_trigger_*`, `windmill-dev_schedule_*`) +7. **app-specialist.md** - Application management (`windmill-dev_app_*`) +8. **workspace-specialist.md** - Workspaces & folders (`windmill-dev_workspace_*`, `windmill-dev_folder_*`) +9. **audit-specialist.md** - Audit logs & security (`windmill-dev_audit_*`) +10. **integration-specialist.md** - OAuth, webhooks, external services (`windmill-dev_integration_*`) +11. **storage-specialist.md** - Database & file storage (`windmill-dev_storage_*`) +12. **system-specialist.md** - System health & configuration (`windmill-dev_system_*`, `windmill-dev_settings_*`) + +Each specialist configuration includes: + +- YAML frontmatter with name, description, mode, model, and tool access patterns +- Detailed instructions on responsibilities +- Common tasks and best practices +- Glob patterns for tool access (e.g., `"windmill-dev_job_*": true`) + +### 5. Build Process Integration + +Updated `package.json` to run the complete workflow: + +```bash +npm run generate +``` + +This single command executes: + +1. `pregenerate`: Fetch latest OpenAPI spec from Windmill +2. `generate`: Run openapi-mcp-generator +3. `postgenerate`: + - Add tool namespaces + - Apply custom overrides + - Build TypeScript + - Generate tool list documentation + +## Tool Categories + +The 59 namespace:subgroup combinations cover: + +- **app**: hub, list, manage +- **audit**: list, search +- **capture**: manage +- **draft**: manage +- **flow**: hub, list, manage, preview, trigger +- **folder**: list, manage +- **group**: list, manage +- **integration**: oauth, slack, teams, github, webhook +- **job**: flow, list, manage, queue, script +- **misc**: general, system +- **oauth**: manage +- **resource**: hub, list, manage, type +- **schedule**: list, manage +- **script**: dependencies, hub, list, manage +- **settings**: instance, global +- **storage**: database, file, s3 +- **system**: health, config +- **trigger**: manage +- **user**: admin, auth, group, profile, token, usage +- **variable**: list, manage +- **workspace**: admin, invite, list, manage, settings, usage +- **worker**: manage, tag + +## Verification + +### Tool Namespace Addition โœ… + +```bash +# Check that tools have namespaces +grep -o 'name: "[^"]*",' build/src/index.ts | head -5 +``` + +Expected output shows namespaced tools: + +``` +name: "settings_system_backendVersion", +name: "system_health_backendUptodate", +name: "audit_list_getAuditLog", +``` + +### Generated Documentation โœ… + +```bash +# Check generated tool list +wc -l docs/generated-tools.md +grep -c "^## " docs/generated-tools.md +``` + +Results: + +- 693 lines in documentation +- 59 namespace categories +- 501 total tools + +### Agent Configurations โœ… + +```bash +# List all agent configurations +ls -1 .opencode/agent/ +``` + +Expected files: + +- windmill-manager.md (1) +- \*-specialist.md files (12) + +## Usage + +### For Developers + +1. **Generate MCP Server**: + + ```bash + npm run generate + ``` + +2. **Run MCP Server**: + + ```bash + npm run dev + ``` + +3. **Review Generated Tools**: + ```bash + cat docs/generated-tools.md + ``` + +### For Agents + +Agents automatically access tools based on their configuration: + +- **windmill-manager**: Can delegate to any specialist +- **job-specialist**: Only has access to `windmill-dev_job_*` tools +- **user-specialist**: Only has access to `windmill-dev_user_*` tools +- etc. + +This ensures: + +- **Separation of concerns**: Each agent focuses on its domain +- **Security**: Agents can only access tools relevant to their role +- **Maintainability**: Tool access is managed via glob patterns, not explicit lists + +## Next Steps + +1. **Test the MCP server** with actual Windmill instance +2. **Validate agent tool access** in practice +3. **Create example workflows** using the agent team +4. **Add integration tests** for agent coordination +5. **Document common agent workflows** and patterns + +## Key Design Decisions + +### 1. Namespace Format + +Chose `namespace_subgroup_operationId` format for: + +- Clear categorization +- Easy glob pattern matching +- Human-readable tool names +- Maintains original operationId for reference + +### 2. Deny-by-Default Security + +All agents start with `"*": false` and explicitly enable tool groups: + +- Prevents accidental tool access +- Makes permissions explicit and auditable +- Follows principle of least privilege + +### 3. Shared Categorization Logic + +The same regex patterns are used in both: + +- `generate-tool-list.js` (documentation) +- `add-tool-namespaces.js` (tool naming) + +This ensures consistency between documentation and actual tool names. + +### 4. Automatic Documentation Generation + +Tool list is regenerated on every build: + +- Always up-to-date with OpenAPI spec +- No manual documentation maintenance +- Clear warning about auto-generation + +## Files Modified + +### Created + +- `src/generator/generate-tool-list.js` +- `src/overrides/add-tool-namespaces.js` +- `docs/generated-tools.md` +- `docs/windmill-agent-team-plan.md` +- `docs/agent-setup-complete.md` +- `.opencode/agent/windmill-manager.md` +- `.opencode/agent/job-specialist.md` +- `.opencode/agent/user-specialist.md` +- `.opencode/agent/script-specialist.md` +- `.opencode/agent/flow-specialist.md` +- `.opencode/agent/resource-specialist.md` +- `.opencode/agent/trigger-specialist.md` +- `.opencode/agent/app-specialist.md` +- `.opencode/agent/workspace-specialist.md` +- `.opencode/agent/audit-specialist.md` +- `.opencode/agent/integration-specialist.md` +- `.opencode/agent/storage-specialist.md` +- `.opencode/agent/system-specialist.md` + +### Modified + +- `package.json` - Updated `postgenerate` script order +- Added `add-tool-namespaces` and `generate-tool-list` scripts + +## Metrics + +- **Tools**: 501 total +- **Namespaces**: 59 categories +- **Agents**: 13 total (1 manager + 12 specialists) +- **Agent configs**: All complete with YAML frontmatter and instructions +- **Build time**: ~2 minutes for full generation workflow +- **Lines of code**: ~5,600 in generated MCP server + +--- + +**Status**: โœ… Complete and ready for testing + +**Date**: 2025-11-18 + +**Windmill Version**: 1.520.1 diff --git a/docs/generated-tools.md b/docs/generated-tools.md new file mode 100644 index 0000000..db62dfd --- /dev/null +++ b/docs/generated-tools.md @@ -0,0 +1,2244 @@ +# Windmill API Tools + +> **โš ๏ธ Auto-generated Document** +> +> This file is automatically generated from the Windmill OpenAPI specification. +> Do not edit this file directly. It will be overwritten during the build process. +> +> Generated on: 2025-11-19T15:37:29.884Z +> Windmill Version: 1.520.1 + +This document lists all available tools generated from the Windmill API OpenAPI specification. + +## app:hub + +### `getHubAppById` + +get hub app by id + +### `listHubApps` + +list all hub apps + + +## app:list + +### `existsApp` + +does an app exisst at path + +### `existsRawApp` + +does an app exisst at path + +### `getAppByPath` + +get app by path + +### `getAppByPathWithDraft` + +get app by path with draft + +### `getAppByVersion` + +get app by version + +### `getAppHistoryByPath` + +get app history by path + +### `getAppLatestVersion` + +get apps's latest version + +### `getAppLiteByPath` + +get app lite by path + +### `getPublicAppByCustomPath` + +get public app by custom path + +### `getPublicAppBySecret` + +get public app by secret + +### `getPublicSecretOfApp` + +get public secret of app + +### `getRawAppData` + +get app by path + +### `listAppPathsFromWorkspaceRunnable` + +list app paths from workspace runnable + +### `listApps` + +list all apps + +### `listRawApps` + +list all raw apps + + +## app:manage + +### `createApp` + +create app + +### `createAppRaw` + +create app raw + +### `createRawApp` + +create raw app + +### `deleteApp` + +delete app + +### `deleteRawApp` + +delete raw app + +### `updateApp` + +update app + +### `updateAppHistory` + +update app history + +### `updateAppRaw` + +update app + + +## audit:list + +### `getAuditLog` + +get audit log (requires admin privilege) + +### `listAuditLogs` + +list audit logs (requires admin privilege) + + +## audit:search + +### `clearIndex` + +Restart container and delete the index to recreate it. + +### `countSearchLogsIndex` + +Search and count the log line hits on every provided host + +### `ListAvailableScopes` + +list of available scopes + +### `searchLogsIndex` + +Search through service logs with a string query + + +## capture:manage + +### `deleteCapture` + +delete a capture + +### `getCapture` + +get a capture + +### `getCaptureConfigs` + +get capture configs for a script or flow + +### `listCaptures` + +list captures for a script or flow + +### `moveCapturesAndConfigs` + +move captures and configs for a script or flow + +### `setCaptureConfig` + +set capture config + + +## draft:manage + +### `createDraft` + +create draft + +### `deleteDraft` + +delete draft + + +## flow:hub + +### `getHubFlowById` + +get hub flow by id + +### `listHubFlows` + +list all hub flows + + +## flow:list + +### `existsFlowByPath` + +exists flow by path + +### `getFlowByPath` + +get flow by path + +### `getFlowByPathWithDraft` + +get flow by path with draft + +### `getFlowDebugInfo` + +get flow debug info + +### `getFlowDeploymentStatus` + +get flow deployment status + +### `getFlowHistory` + +get flow history by path + +### `getFlowLatestVersion` + +get flow's latest version + +### `getFlowUserState` + +get flow user state at a given key + +### `getFlowVersion` + +get flow version + +### `listFlowPaths` + +list all flow paths + +### `listFlowPathsFromWorkspaceRunnable` + +list flow paths from workspace runnable + +### `listFlows` + +list all flows + + +## flow:manage + +### `archiveFlowByPath` + +archive flow by path + +### `createFlow` + +create flow + +### `deleteFlowByPath` + +delete flow by path + +### `updateFlow` + +update flow + +### `updateFlowHistory` + +update flow history + + +## flow:run + +### `restartFlowAtStep` + +restart a completed flow at a given step + + +## folder:list + +### `existsFolder` + +exists folder + +### `getFolder` + +get folder + +### `getFolderUsage` + +get folder usage + +### `listFolderNames` + +list folder names + +### `listFolders` + +list folders + + +## folder:manage + +### `createFolder` + +create folder + +### `deleteFolder` + +delete folder + +### `updateFolder` + +update folder + + +## folder:permissions + +### `addGranularAcls` + +add granular acls + +### `addOwnerToFolder` + +add owner to folder + +### `getGranularAcls` + +get granular acls + +### `removeGranularAcls` + +remove granular acls + +### `removeOwnerToFolder` + +remove owner to folder + + +## group:concurrency + +### `deleteConcurrencyGroup` + +Delete concurrency group + +### `listConcurrencyGroups` + +List all concurrency groups + + +## group:list + +### `getGroup` + +get group + +### `listGroupNames` + +list group names + +### `listGroups` + +list groups + + +## group:manage + +### `createGroup` + +create group + +### `deleteGroup` + +delete group + +### `updateGroup` + +update group + + +## input:manage + +### `createInput` + +Create an Input for future use in a script or flow + +### `deleteInput` + +Delete a Saved Input + +### `getArgsFromHistoryOrSavedInput` + +Get args from history or saved input + +### `getInputHistory` + +List Inputs used in previously completed jobs + +### `listInputs` + +List saved Inputs for a Runnable + +### `updateInput` + +Update an Input + + +## integration:gcp + +### `deleteGcpSubscription` + +delete gcp trigger + +### `listAllTGoogleTopicSubscriptions` + +list all subscription of a give topic from google cloud service + +### `listGoogleTopics` + +list all topics of google cloud service + + +## integration:github + +### `deleteGitSyncRepository` + +delete individual git sync repository + +### `editGitSyncRepository` + +add or update individual git sync repository + + +## integration:hub + +### `listHubIntegrations` + +list hub integrations + + +## integration:oauth + +### `connectCallback` + +connect callback + +### `connectClientCredentials` + +connect OAuth using client credentials + +### `connectSlackCallback` + +connect slack callback + +### `connectSlackCallbackInstance` + +connect slack callback instance + +### `connectTeams` + +connect teams + +### `disconnectAccount` + +disconnect account + +### `disconnectSlack` + +disconnect slack + +### `disconnectTeams` + +disconnect teams + +### `getGlobalConnectedRepositories` + +get connected repositories + +### `getOAuthConnect` + +get oauth connect + +### `listOAuthConnects` + +list oauth connects + +### `polarsConnectionSettings` + +Converts an S3 resource to the set of arguments necessary to connect Polars to an S3 bucket + +### `polarsConnectionSettingsV2` + +Converts an S3 resource to the set of arguments necessary to connect Polars to an S3 bucket + + +## integration:slack + +### `editSlackCommand` + +edit slack command + +### `getSlackApprovalPayload` + +generate interactive slack approval for suspended job + + +## integration:teams + +### `editTeamsCommand` + +edit teams command + +### `getTeamsApprovalPayload` + +generate interactive teams approval for suspended job + +### `listAvailableTeamsChannels` + +list available teams channels + +### `listAvailableTeamsIds` + +list available teams ids + +### `syncTeams` + +synchronize Microsoft Teams information (teams/channels) + + +## job:list + +### `countCompletedJobs` + +count number of completed jobs with filter + +### `countJobsByTag` + +Count jobs by tag + +### `getCompletedCount` + +get completed count + +### `getCompletedJob` + +get completed job + +### `getCompletedJobResult` + +get completed job result + +### `getCompletedJobResultMaybe` + +get completed job result if job is completed + +### `getJob` + +get job + +### `getJob logs` + +get job logs + +### `getJobArgs` + +get job args + +### `getJobMetrics` + +get job metrics + +### `getJobProgress` + +get job progress + +### `getJobUpdates` + +get job updates + +### `getJobUpdatesSSE` + +get job updates via server-sent events + +### `listCompletedJobs` + +list all completed jobs + +### `listExtendedJobs` + +Get intervals of job runtime concurrency + +### `listFilteredJobsUuids` + +get the ids of all jobs matching the given filters + +### `listJobs` + +list all jobs + +### `searchJobsIndex` + +Search through jobs with a string query + + +## job:manage + +### `batchReRunJobs` + +re-run multiple jobs + +### `cancelPersistentQueuedJobs` + +cancel all queued jobs for persistent script + +### `cancelQueuedJob` + +cancel queued or running job + +### `cancelSelection` + +cancel jobs based on the given uuids + +### `cancelSuspendedJobGet` + +cancel a job for a suspended flow + +### `cancelSuspendedJobPost` + +cancel a job for a suspended flow + +### `createJobSignature` + +create an HMac signature given a job id and a resume id + +### `deleteCompletedJob` + +delete completed job (erase content but keep run id) + +### `forceCancelQueuedJob` + +force cancel queued job + +### `getConcurrencyKey` + +Get the concurrency key for a job that has concurrency limits enabled + +### `getResumeUrls` + +get resume urls given a job_id, resume_id and a nonce to resume a flow + +### `getRootJobId` + +get root job id + +### `getSuspendedJobFlow` + +get parent flow job of suspended job + +### `resumeSuspendedFlowAsOwner` + +resume a job for a suspended flow as an owner + +### `resumeSuspendedJobGet` + +resume a job for a suspended flow + +### `resumeSuspendedJobPost` + +resume a job for a suspended flow + +### `setJobProgress` + +set job metrics + + +## job:run + +### `runFlowByPath` + +run flow by path + +### `runFlowPreview` + +run flow preview + +### `runScriptByHash` + +run script by hash + +### `runScriptByPath` + +run script by path + +### `runScriptPreview` + +run script preview + +### `runSlackMessageTestJob` + +run a job that sends a message to Slack + +### `runTeamsMessageTestJob` + +run a job that sends a message to Teams + +### `runWaitResultFlowByPath` + +run flow by path and wait until completion + +### `runWaitResultScriptByPath` + +run script by path + +### `runWaitResultScriptByPathGet` + +run script by path with get + + +## misc:general + +### `getLatestKeyRenewalAttempt` + +get latest key renewal attempt + +### `listSearchApp` + +list apps for search + +### `listSearchFlow` + +list flows for search + +### `listSearchResource` + +list resources for search + +### `listSearchScript` + +list scripts for search + +### `listSelectedJobGroups` + +list selected jobs script/flow schemas grouped by (kind, path) + +### `listUsernames` + +list usernames + +### `listUsers` + +list users + +### `listUsersAsSuperAdmin` + +list all users as super admin (require to be super amdin) + +### `listUsersUsage` + +list users usage + +### `loadCsvPreview` + +Load a preview of a csv file + +### `loadParquetPreview` + +Load a preview of a parquet file + +### `loadTableRowCount` + +Load the table row count + +### `openaiSyncFlowByPath` + +run flow by path and wait until completion in openai format + +### `openaiSyncScriptByPath` + +run script by path in openai format + +### `overwriteInstanceGroups` + +overwrite instance groups + +### `rawScriptByHash` + +raw script by hash + +### `rawScriptByPath` + +raw script by path + +### `renewLicenseKey` + +renew license key + +### `resultById` + +get job result by id + +### `runCodeWorkflowTask` + +run code-workflow task + +### `s3ResourceInfo` + +Returns the s3 resource associated to the provided path, or the workspace default S3 resource + +### `sendMessageToConversation` + +send update to Microsoft Teams activity + +### `setDefaultErrorOrRecoveryHandler` + +Set default error or recoevery handler + +### `setFlowUserState` + +set flow user state at a given key + +### `setGlobal` + +post global settings + +### `setThresholdAlert` + +set threshold alert info + +### `signS3Objects` + +sign s3 objects, to be used by anonymous users in public apps + +### `star` + +star item + +### `testCriticalChannels` + +test critical channels + +### `testLicenseKey` + +test license key + +### `testMetadata` + +test metadata + +### `testSmtp` + +test smtp + +### `toggleWorkspaceErrorHandlerForFlow` + +Toggle ON and OFF the workspace error handler for a given flow + +### `toggleWorkspaceErrorHandlerForScript` + +Toggle ON and OFF the workspace error handler for a given script + +### `unstar` + +unstar item + +### `updateInstanceGroup` + +update instance group + +### `updateOperatorSettings` + +Update operator settings for a workspace + +### `updateRawApp` + +update app + +### `uploadS3FileFromApp` + +upload s3 file from app + +### `workspaceMuteCriticalAlertsUI` + +Mute critical alert UI for this workspace + + +## resource:list + +### `existsResource` + +does resource exists + +### `existsResourceType` + +does resource_type exists + +### `getResource` + +get resource + +### `getResourceType` + +get resource_type + +### `getResourceValue` + +get resource value + +### `getResourceValueInterpolated` + +get resource interpolated (variables and resources are fully unrolled) + +### `listResource` + +list resources + +### `listResourceNames` + +list resource names + +### `listResourceType` + +list resource_types + +### `listResourceTypeNames` + +list resource_types names + +### `queryResourceTypes` + +query resource types by similarity + + +## resource:manage + +### `createResource` + +create resource + +### `createResourceType` + +create resource_type + +### `deleteResource` + +delete resource + +### `deleteResourceType` + +delete resource_type + +### `updateResource` + +update resource + +### `updateResourceType` + +update resource_type + +### `updateResourceValue` + +update resource value + + +## resource:types + +### `fileResourceTypeToFileExtMap` + +get map from resource type to format extension + + +## schedule:list + +### `existsSchedule` + +does schedule exists + +### `getSchedule` + +get schedule + +### `listSchedules` + +list schedules + +### `listSchedulesWithJobs` + +list schedules with last 20 jobs + + +## schedule:manage + +### `createSchedule` + +create schedule + +### `deleteSchedule` + +delete schedule + +### `previewSchedule` + +preview schedule + +### `setScheduleEnabled` + +set enabled schedule + +### `updateSchedule` + +update schedule + + +## script:hub + +### `getHubScriptByPath` + +get full hub script by path + +### `getHubScriptContentByPath` + +get hub script content by path + +### `getTopHubScripts` + +get top hub scripts + +### `queryHubScripts` + +query hub scripts by similarity + + +## script:list + +### `customPathExists` + +check if custom path exists + +### `existsRoute` + +does route exists + +### `existsScriptByPath` + +exists script by path + +### `getRunnable` + +get all runnables in every workspace + +### `getScriptByHash` + +get script by hash + +### `getScriptByPath` + +get script by path + +### `getScriptByPathWithDraft` + +get script by path with draft + +### `getScriptDeploymentStatus` + +get script deployment status + +### `getScriptHistoryByPath` + +get history of a script by path + +### `getScriptLatestVersion` + +get scripts's latest version (hash) + +### `isOwnerOfPath` + +is owner of path + +### `listScriptPaths` + +list all scripts paths + +### `listScriptPathsFromWorkspaceRunnable` + +list script paths using provided script as a relative import + +### `listScripts` + +list all scripts + + +## script:manage + +### `archiveScriptByHash` + +archive script by hash + +### `archiveScriptByPath` + +archive script by path + +### `createScript` + +create script + +### `deleteScriptByHash` + +delete script by hash (erase content but keep hash, require admin) + +### `deleteScriptByPath` + +delete script at a given path (require admin) + +### `executeComponent` + +executeComponent + +### `updateScriptHistory` + +update history of a script + + +## script:run + +### `runRawScriptDependencies` + +run a one-off dependencies job + + +## settings:general + +### `deleteConfig` + +Delete Config + +### `editCopilotConfig` + +edit copilot config + +### `editDefaultScripts` + +edit default scripts for workspace + +### `editDeployTo` + +edit deploy to + +### `editErrorHandler` + +edit error handler + +### `editWebhook` + +edit webhook + +### `get config` + +get config + +### `get default scripts` + +get default scripts for workspace + +### `getCopilotInfo` + +get copilot info + +### `getDeployTo` + +get deploy to + +### `getGlobal` + +get global settings + +### `getLocal` + +get local settings + +### `getSettings` + +get settings + +### `listConfigs` + +list configs + +### `listGlobalSettings` + +list global settings + +### `updateConfig` + +Update config + + +## settings:storage + +### `createDucklakeDatabase` + +Runs CREATE DATABASE on the Windmill Postgres and grants access to the ducklake_user + +### `editDucklakeConfig` + +edit ducklake settings + +### `editLargeFileStorageConfig` + +edit large file storage settings + +### `getLargeFileStorageConfig` + +get large file storage config + +### `getSecondaryStorageNames` + +get secondary storage names + +### `listDucklakes` + +list ducklakes + +### `testObjectStorageConfig` + +test object storage config + + +## settings:system + +### `backendVersion` + +get backend version + +### `geDefaultTags` + +get all instance default tags + +### `getCustomTags` + +get all instance custom tags (tags are used to dispatch jobs to different worker groups) + +### `getLicenseId` + +get license id + +### `getUsage` + +get current usage outside of premium workspaces + +### `isDefaultTagsPerWorkspace` + +is default tags per workspace + +### `sendStats` + +send stats + + +## storage:db + +### `createPostgresPublication` + +create publication for postgres + +### `createPostgresReplicationSlot` + +create replication slot for postgres + +### `databasesExist` + +checks that all given databases exist or else return the ones that don't + +### `deletePostgresPublication` + +delete postgres publication + +### `deletePostgresReplicationSlot` + +delete postgres replication slot + +### `duckdbConnectionSettings` + +Converts an S3 resource to the set of instructions necessary to connect DuckDB to an S3 bucket + +### `duckdbConnectionSettingsV2` + +Converts an S3 resource to the set of instructions necessary to connect DuckDB to an S3 bucket + +### `getDbClock` + +get db clock + +### `getPostgresPublication` + +get postgres publication + +### `getPostgresVersion` + +get postgres version + +### `isValidPostgresConfiguration` + +check if postgres configuration is set to logical + +### `listPostgresPublication` + +list postgres publication + +### `listPostgresReplicationSlot` + +list postgres replication slot + +### `updatePostgresPublication` + +update publication for postgres + + +## storage:list + +### `fileDownload` + +Download file from S3 bucket + +### `fileDownloadParquetAsCsv` + +Download file to S3 bucket + +### `getLogFile` + +get log file by path + +### `getLogFileFromStore` + +get log file from object store + +### `listAssets` + +List all assets in the workspace + +### `listAssetsByUsage` + +List all assets used by given usages paths + +### `listLogFiles` + +list log files ordered by timestamp + +### `listStoredFiles` + +List the file keys available in a workspace object storage + +### `loadFileMetadata` + +Load metadata of the file + +### `loadFilePreview` + +Load a preview of the file + + +## storage:manage + +### `deleteS3File` + +Permanently delete file from S3 + +### `deleteS3FileFromApp` + +delete s3 file from app + +### `fileUpload` + +Upload file to S3 bucket + +### `get public resource` + +get public resource + +### `moveS3File` + +Move a S3 file from one path to the other within the same bucket + + +## system:api + +### `DownloadOpenapiSpec` + +Download the OpenAPI v3.1 spec as a file + +### `generateOpenapiSpec` + +generate openapi spec from http routes/webhook + +### `getOpenApiYaml` + +get openapi yaml spec + +### `listAvailablePythonVersions` + +Get currently available python versions provided by UV. + +### `listMcpTools` + +list available MCP tools + + +## system:critical + +### `acknowledgeAllCriticalAlerts` + +Acknowledge all unacknowledged critical alerts + +### `acknowledgeCriticalAlert` + +Acknowledge a critical alert + +### `getCriticalAlerts` + +Get all critical alerts + +### `getThresholdAlert` + +get threshold alert info + +### `workspaceGetCriticalAlerts` + +Get all critical alerts for this workspace + + +## system:health + +### `backendUptodate` + +is backend up to date + +### `pingCaptureConfig` + +ping capture config + + +## system:instance + +### `createCustomerPortalSession` + +create customer portal session + +### `exportInstallation` + +Export GitHub installation JWT token + +### `getIsPremium` + +get if workspace is premium + +### `getPremiumInfo` + +get premium info + +### `importInstallation` + +Import GitHub installation from JWT token + +### `isDomainAllowed` + +is domain allowed for auto invi + +### `leaveInstance` + +leave instance + + +## system:usage + +### `listAutoscalingEvents` + +List autoscaling events + + +## system:workers + +### `existsWorkerWithTag` + +exists worker with tag + +### `getCountsOfJobsWaitingPerTag` + +get counts of jobs waiting for an executor per tag + +### `getQueueCount` + +get queue count + +### `getQueueMetrics` + +get queue metrics + +### `listFilteredQueueUuids` + +get the ids of all queued jobs matching the given filters + +### `listQueue` + +list all queued jobs + +### `listWorkerGroups` + +list worker groups + +### `listWorkers` + +list workers + + +## template:manage + +### `createTemplateScript` + +create template script + +### `getTemplateScript` + +get template script + + +## trigger:list + +### `existsGcpTrigger` + +does gcp trigger exists + +### `existsHttpTrigger` + +does http trigger exists + +### `existsKafkaTrigger` + +does kafka trigger exists + +### `existsMqttTrigger` + +does mqtt trigger exists + +### `existsNatsTrigger` + +does nats trigger exists + +### `existsPostgresTrigger` + +does postgres trigger exists + +### `existsSqsTrigger` + +does sqs trigger exists + +### `existsWebsocketTrigger` + +does websocket trigger exists + +### `getGcpTrigger` + +get gcp trigger + +### `getHttpTrigger` + +get http trigger + +### `getKafkaTrigger` + +get kafka trigger + +### `getMqttTrigger` + +get mqtt trigger + +### `getNatsTrigger` + +get nats trigger + +### `getPostgresTrigger` + +get postgres trigger + +### `getSqsTrigger` + +get sqs trigger + +### `getTriggersCountOfFlow` + +get triggers count of flow + +### `getTriggersCountOfScript` + +get triggers count of script + +### `getUsedTriggers` + +get used triggers + +### `getWebsocketTrigger` + +get websocket trigger + +### `listGcpTriggers` + +list gcp triggers + +### `listHttpTriggers` + +list http triggers + +### `listKafkaTriggers` + +list kafka triggers + +### `listMqttTriggers` + +list mqtt triggers + +### `listNatsTriggers` + +list nats triggers + +### `listPostgresTriggers` + +list postgres triggers + +### `listSqsTriggers` + +list sqs triggers + +### `listWebsocketTriggers` + +list websocket triggers + + +## trigger:manage + +### `createGcpTrigger` + +create gcp trigger + +### `createHttpTrigger` + +create http trigger + +### `createHttpTriggers` + +create many HTTP triggers + +### `createKafkaTrigger` + +create kafka trigger + +### `createMqttTrigger` + +create mqtt trigger + +### `createNatsTrigger` + +create nats trigger + +### `createPostgresTrigger` + +create postgres trigger + +### `createSqsTrigger` + +create sqs trigger + +### `createWebsocketTrigger` + +create websocket trigger + +### `deleteGcpTrigger` + +delete gcp trigger + +### `deleteHttpTrigger` + +delete http trigger + +### `deleteKafkaTrigger` + +delete kafka trigger + +### `deleteMqttTrigger` + +delete mqtt trigger + +### `deleteNatsTrigger` + +delete nats trigger + +### `deletePostgresTrigger` + +delete postgres trigger + +### `deleteSqsTrigger` + +delete sqs trigger + +### `deleteWebsocketTrigger` + +delete websocket trigger + +### `setGcpTriggerEnabled` + +set enabled gcp trigger + +### `setKafkaTriggerEnabled` + +set enabled kafka trigger + +### `setMqttTriggerEnabled` + +set enabled mqtt trigger + +### `setNatsTriggerEnabled` + +set enabled nats trigger + +### `setPostgresTriggerEnabled` + +set enabled postgres trigger + +### `setSqsTriggerEnabled` + +set enabled sqs trigger + +### `setWebsocketTriggerEnabled` + +set enabled websocket trigger + +### `updateGcpTrigger` + +update gcp trigger + +### `updateHttpTrigger` + +update http trigger + +### `updateKafkaTrigger` + +update kafka trigger + +### `updateMqttTrigger` + +update mqtt trigger + +### `updateNatsTrigger` + +update nats trigger + +### `updatePostgresTrigger` + +update postgres trigger + +### `updateSqsTrigger` + +update sqs trigger + +### `updateWebsocketTrigger` + +update websocket trigger + + +## trigger:test + +### `datasetStorageTestConnection` + +Test connection to the workspace object storage + +### `testGcpConnection` + +test gcp connection + +### `testKafkaConnection` + +test kafka connection + +### `testMqttConnection` + +test mqtt connection + +### `testNatsConnection` + +test NATS connection + +### `testPostgresConnection` + +test postgres connection + +### `testSqsConnection` + +test sqs connection + +### `testWebsocketConnection` + +test websocket connection + + +## tutorial:manage + +### `getTutorialProgress` + +get tutorial progress + +### `updateTutorialProgress` + +update tutorial progress + + +## user:admin + +### `addUser` + +add user to workspace + +### `addUserToGroup` + +add user to group + +### `addUserToInstanceGroup` + +add user to instance group + +### `createAccount` + +create OAuth account + +### `createUserGlobally` + +create user + +### `deleteUser` + +delete user (require admin privilege) + +### `existsEmail` + +exists email + +### `existsUsername` + +exists username + +### `globalUserDelete` + +global delete user (require super admin) + +### `globalUsernameInfo` + +global username info (require super admin) + +### `globalUserRename` + +global rename user (require super admin) + +### `globalUsersExport` + +global export users (require super admin and EE) + +### `globalUsersOverwrite` + +global overwrite users (require super admin and EE) + +### `globalUserUpdate` + +global update user (require super admin) + + +## user:auth + +### `blacklistAgentToken` + +blacklist agent token (requires super admin) + +### `createAgentToken` + +create agent token + +### `createToken` + +create token + +### `createTokenImpersonate` + +create token to impersonate a user (require superadmin) + +### `deleteToken` + +delete token + +### `getGithubAppToken` + +get github app token + +### `getOidcToken` + +get OIDC token (ee only) + +### `listBlacklistedAgentTokens` + +list blacklisted agent tokens (requires super admin) + +### `listOAuthLogins` + +list oauth logins + +### `listTokens` + +list token + +### `listTokensOfFlow` + +get tokens with flow scope + +### `listTokensOfScript` + +get tokens with script scope + +### `login` + +login with password + +### `loginWithOauth` + +login with oauth authorization flow + +### `logout` + +logout + +### `rawScriptByPathTokened` + +raw script by path with a token (mostly used by lsp to be used with import maps to resolve scripts) + +### `refreshToken` + +refresh token + +### `refreshUserToken` + +refresh the current token + +### `removeBlacklistAgentToken` + +remove agent token from blacklist (requires super admin) + +### `setLoginTypeForUser` + +set login type for a specific user (require super admin) + +### `setPassword` + +set password + +### `setPasswordForUser` + +set password for a specific user (require super admin) + + +## user:groups + +### `createInstanceGroup` + +create instance group + +### `deleteInstanceGroup` + +delete instance group + +### `exportInstanceGroups` + +export instance groups + +### `getInstanceGroup` + +get instance group + +### `listInstanceGroups` + +list instance groups + +### `removeUserFromInstanceGroup` + +remove user from instance group + +### `removeUserToGroup` + +remove user to group + + +## user:invites + +### `acceptInvite` + +accept invite to workspace + +### `declineInvite` + +decline invite to workspace + +### `delete invite` + +delete user invite + +### `editAutoInvite` + +edit auto invite + +### `inviteUser` + +invite user to workspace + +### `listPendingInvites` + +list pending invites for a workspace + +### `listWorkspaceInvites` + +list all workspace invites + + +## user:profile + +### `getCurrentEmail` + +get current user email (if logged in) + +### `getUser` + +get user (require admin privilege) + +### `globalWhoami` + +get current global whoami (if logged in) + +### `updateUser` + +update user (require admin privilege) + +### `usernameToEmail` + +get email from username + +### `whoami` + +whoami + +### `whois` + +whois + + +## variable:list + +### `encryptValue` + +encrypt value + +### `existsVariable` + +does variable exists at path + +### `getVariable` + +get variable + +### `getVariableValue` + +get variable value + +### `listContextualVariables` + +list contextual variables + +### `listVariable` + +list variables + + +## variable:manage + +### `createVariable` + +create variable + +### `deleteVariable` + +delete variable + +### `setEnvironmentVariable` + +set environment variable + +### `updateVariable` + +update variable + + +## workspace:list + +### `existsWorkspace` + +exists workspace + +### `getWorkspaceDefaultApp` + +get default app for workspace + +### `getWorkspaceEncryptionKey` + +retrieves the encryption key for this workspace + +### `getWorkspaceName` + +get workspace name + +### `getWorkspaceUsage` + +get usage + +### `listUserWorkspaces` + +list all workspaces visible to me with user info + +### `listWorkspaces` + +list all workspaces visible to me + +### `listWorkspacesAsSuperAdmin` + +list all workspaces as super admin (require to be super admin) + + +## workspace:manage + +### `archiveWorkspace` + +archive workspace + +### `changeWorkspaceColor` + +change workspace id + +### `changeWorkspaceId` + +change workspace id + +### `changeWorkspaceName` + +change workspace name + +### `createWorkspace` + +create workspace + +### `deleteFromWorkspace` + +Delete a GitHub installation from a workspace + +### `deleteWorkspace` + +delete workspace (require super admin) + +### `installFromWorkspace` + +Install a GitHub installation from another workspace + +### `leaveWorkspace` + +leave workspace + +### `unarchiveWorkspace` + +unarchive workspace + + +## workspace:settings + +### `editWorkspaceDefaultApp` + +edit default app for workspace + +### `editWorkspaceDeployUISettings` + +edit workspace deploy ui settings + +### `editWorkspaceGitSyncConfig` + +edit workspace git sync settings + +### `setWorkspaceEncryptionKey` + +update the encryption key for this workspace + + +## workspace:usage + +### `workspaceAcknowledgeAllCriticalAlerts` + +Acknowledge all unacknowledged critical alerts for this workspace + +### `workspaceAcknowledgeCriticalAlert` + +Acknowledge a critical alert for this workspace + + +--- + +Total tools: 501 + +## Tool to Agent Mapping Guide + +This document categorizes all Windmill API tools by functional area. Each category maps to specific OpenCode agents: + +- **job:** โ†’ job-specialist +- **user:** โ†’ user-specialist +- **workspace:** โ†’ workspace-specialist +- **script:** โ†’ script-specialist +- **flow:** โ†’ flow-specialist +- **resource:** โ†’ resource-specialist +- **variable:** โ†’ storage-specialist +- **schedule:** โ†’ trigger-specialist +- **trigger:** โ†’ trigger-specialist +- **app:** โ†’ app-specialist +- **folder:** โ†’ workspace-specialist +- **group:** โ†’ user-specialist +- **storage:** โ†’ storage-specialist +- **integration:** โ†’ integration-specialist +- **audit:** โ†’ audit-specialist +- **settings:** โ†’ system-specialist +- **system:** โ†’ system-specialist +- **capture:** โ†’ system-specialist +- **input:** โ†’ system-specialist +- **draft:** โ†’ system-specialist +- **template:** โ†’ system-specialist +- **tutorial:** โ†’ system-specialist +- **misc:** โ†’ Review category - assign to most relevant specialist + +Use this reference when routing tool requests to the appropriate agent. diff --git a/docs/json-schema-manual-guide.md b/docs/json-schema-manual-guide.md new file mode 100644 index 0000000..8f46c2f --- /dev/null +++ b/docs/json-schema-manual-guide.md @@ -0,0 +1,664 @@ +# JSON Schema Manual Guide for Windmill API/MCP Operations + +## Critical Requirement + +โš ๏ธ **IMPORTANT**: When creating or updating scripts, flows, or apps via the Windmill API or MCP tools, you **MUST** manually provide the JSON Schema for all parameters. Unlike the Windmill UI (which automatically infers schemas from your code), the API requires explicit schema definitions. + +**Failing to provide schemas will result in:** + +- Parameters not appearing in the Windmill UI +- Users unable to configure or run your scripts/flows +- Silent failures where the resource exists but is unusable + +## Why This Matters + +### UI vs API Behavior + +| Method | Schema Inference | What You Must Do | +| -------------------- | ---------------- | ------------------------------------------------- | +| **Windmill UI** | โœ… Automatic | Write code with type hints; UI detects parameters | +| **Windmill API/MCP** | โŒ Manual | Provide complete JSON Schema in `schema` field | + +### Example: The Problem + +```typescript +// When you create this script via API: +{ + "path": "f/examples/hello", + "content": "def main(name: str, age: int):\n return f'Hello {name}, {age}'" + // โŒ Missing "schema" field +} +``` + +**Result**: Script exists in Windmill, but the UI shows no parameters. Users cannot provide `name` or `age` values. + +### Example: The Solution + +```typescript +// Correct API usage with schema: +{ + "path": "f/examples/hello", + "content": "def main(name: str, age: int):\n return f'Hello {name}, {age}'", + "schema": { + "type": "object", + "required": ["name", "age"], + "properties": { + "name": { + "type": "string", + "description": "User's name" + }, + "age": { + "type": "integer", + "description": "User's age" + } + } + } +} +``` + +**Result**: โœ… Script is fully functional. UI displays both parameters with proper types and descriptions. + +## How to Build JSON Schemas + +### Basic Structure + +All Windmill parameter schemas follow this structure: + +```json +{ + "type": "object", + "required": ["param1", "param2"], + "properties": { + "param1": { + "type": "string", + "description": "What this parameter does" + }, + "param2": { + "type": "integer", + "description": "What this parameter does" + } + } +} +``` + +### Type Mapping Reference + +Use these mappings when converting code type hints to JSON Schema: + +#### Python โ†’ JSON Schema + +| Python Type | JSON Schema Type | Example | +| ---------------- | ----------------------------------------- | ----------------------------------------------------------------- | +| `str` | `"type": "string"` | `{"type": "string"}` | +| `int` | `"type": "integer"` | `{"type": "integer"}` | +| `float` | `"type": "number"` | `{"type": "number"}` | +| `bool` | `"type": "boolean"` | `{"type": "boolean"}` | +| `list[str]` | `"type": "array"` + items | `{"type": "array", "items": {"type": "string"}}` | +| `dict` | `"type": "object"` | `{"type": "object"}` | +| `dict[str, int]` | `"type": "object"` + additionalProperties | `{"type": "object", "additionalProperties": {"type": "integer"}}` | + +#### TypeScript โ†’ JSON Schema + +| TypeScript Type | JSON Schema Type | Example | +| ------------------------ | ----------------------------------------- | ---------------------------------------------------------------- | +| `string` | `"type": "string"` | `{"type": "string"}` | +| `number` | `"type": "number"` | `{"type": "number"}` | +| `boolean` | `"type": "boolean"` | `{"type": "boolean"}` | +| `string[]` | `"type": "array"` + items | `{"type": "array", "items": {"type": "string"}}` | +| `object` | `"type": "object"` | `{"type": "object"}` | +| `Record` | `"type": "object"` + additionalProperties | `{"type": "object", "additionalProperties": {"type": "number"}}` | + +### Optional Parameters + +Parameters are optional unless listed in the `required` array: + +```json +{ + "type": "object", + "required": ["username"], + "properties": { + "username": { + "type": "string", + "description": "Required parameter" + }, + "email": { + "type": "string", + "description": "Optional parameter" + } + } +} +``` + +### Default Values + +Add `"default"` to provide default values: + +```json +{ + "type": "object", + "properties": { + "retries": { + "type": "integer", + "description": "Number of retry attempts", + "default": 3 + }, + "timeout": { + "type": "number", + "description": "Timeout in seconds", + "default": 30.0 + } + } +} +``` + +### Enums (Fixed Choices) + +Use `"enum"` for dropdown selections: + +```json +{ + "type": "object", + "properties": { + "environment": { + "type": "string", + "description": "Deployment environment", + "enum": ["dev", "staging", "production"], + "default": "dev" + } + } +} +``` + +### Advanced Features + +#### Windmill Resource Types + +Link to Windmill resources (databases, APIs, etc.): + +```json +{ + "type": "object", + "properties": { + "database": { + "type": "object", + "description": "PostgreSQL connection", + "format": "resource-postgresql" + } + } +} +``` + +Common formats: + +- `resource-postgresql` - PostgreSQL database +- `resource-mysql` - MySQL database +- `resource-s3` - AWS S3 +- `resource-slack` - Slack integration +- `resource-github` - GitHub API + +See [Windmill Resource Types](https://www.windmill.dev/docs/core_concepts/resources_and_types) for full list. + +#### Dynamic Selects (DynSelect) + +Allow users to select from dynamically loaded options: + +```json +{ + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Select a user", + "format": "dynselect", + "dynamicSelectOptions": { + "script": "u/admin/list_users", + "valueKey": "id", + "labelKey": "username" + } + } + } +} +``` + +#### Union Types (oneOf) + +Allow multiple type options: + +```json +{ + "type": "object", + "properties": { + "input": { + "description": "Accept string or number", + "oneOf": [{ "type": "string" }, { "type": "number" }] + } + } +} +``` + +## Step-by-Step Schema Creation + +### Process for Scripts + +1. **Extract parameters from code** + + ```python + def main(name: str, age: int = 25, active: bool = True): + pass + ``` + +2. **Identify required vs optional** + - Required: `name` (no default) + - Optional: `age`, `active` (have defaults) + +3. **Build schema structure** + + ```json + { + "type": "object", + "required": ["name"], + "properties": {} + } + ``` + +4. **Add each parameter** + ```json + { + "type": "object", + "required": ["name"], + "properties": { + "name": { "type": "string", "description": "User's name" }, + "age": { "type": "integer", "description": "User's age", "default": 25 }, + "active": { + "type": "boolean", + "description": "Is user active", + "default": true + } + } + } + ``` + +### Process for Flows + +Flow modules can have input transforms. Provide schema for flow-level inputs: + +```json +{ + "path": "f/examples/my_flow", + "value": { + "modules": [ + { + "id": "a", + "value": { + "type": "script", + "path": "u/admin/process_data", + "input_transforms": { + "data": { "type": "javascript", "expr": "flow_input.raw_data" } + } + } + } + ] + }, + "schema": { + "type": "object", + "required": ["raw_data"], + "properties": { + "raw_data": { + "type": "array", + "description": "Raw data to process", + "items": { "type": "object" } + } + } + } +} +``` + +## Common Mistakes to Avoid + +### โŒ Mistake 1: Forgetting the Schema Entirely + +```typescript +// WRONG - No schema provided +windmill.createScript({ + path: "f/examples/greet", + content: "def main(name: str):\n return f'Hello {name}'", +}); +``` + +### โœ… Fix: Always Include Schema + +```typescript +// CORRECT +windmill.createScript({ + path: "f/examples/greet", + content: "def main(name: str):\n return f'Hello {name}'", + schema: { + type: "object", + required: ["name"], + properties: { + name: { type: "string", description: "Name to greet" }, + }, + }, +}); +``` + +### โŒ Mistake 2: Mismatched Parameter Names + +```typescript +// Code uses "username" but schema says "user_name" +{ + content: "def main(username: str): pass", + schema: { + properties: { + user_name: {type: "string"} // โŒ Wrong name! + } + } +} +``` + +### โœ… Fix: Match Exactly + +```typescript +{ + content: "def main(username: str): pass", + schema: { + properties: { + username: {type: "string"} // โœ… Correct + } + } +} +``` + +### โŒ Mistake 3: Wrong Type Mapping + +```python +# Python code +def main(count: int): pass +``` + +```json +// Wrong schema +{ + "properties": { + "count": { "type": "number" } // โŒ Should be "integer" + } +} +``` + +### โœ… Fix: Use Correct Type + +```json +{ + "properties": { + "count": { "type": "integer" } // โœ… Correct + } +} +``` + +### โŒ Mistake 4: Missing Required Array + +```json +// All parameters are actually required but not marked +{ + "type": "object", + "properties": { + "api_key": { "type": "string" }, + "endpoint": { "type": "string" } + } + // โŒ Missing "required" field +} +``` + +### โœ… Fix: Specify Required Parameters + +```json +{ + "type": "object", + "required": ["api_key", "endpoint"], // โœ… Explicit + "properties": { + "api_key": { "type": "string" }, + "endpoint": { "type": "string" } + } +} +``` + +## Testing Your Schemas + +### Verification Checklist + +After creating a script/flow via API, verify in the Windmill UI: + +1. โœ… Navigate to the script/flow in the UI +2. โœ… Click "Run" or "Test" +3. โœ… Verify all parameters appear in the form +4. โœ… Check that types are correct (text input, number input, checkbox, etc.) +5. โœ… Verify required parameters are marked with asterisks +6. โœ… Test default values pre-fill correctly +7. โœ… Verify descriptions appear as help text + +### Quick Test Script + +Use this pattern to test schema generation: + +```python +# test_schema.py +import json + +def generate_schema(code: str) -> dict: + """Parse code and generate schema""" + # Your schema generation logic here + pass + +# Test +code = "def main(name: str, age: int = 25): pass" +schema = generate_schema(code) +print(json.dumps(schema, indent=2)) + +# Expected output: +# { +# "type": "object", +# "required": ["name"], +# "properties": { +# "name": {"type": "string"}, +# "age": {"type": "integer", "default": 25} +# } +# } +``` + +## Real-World Examples + +### Example 1: Data Processing Script + +**Python Code:** + +```python +def main( + input_file: str, + output_format: str = "json", + compress: bool = False, + max_records: int = 1000 +): + """Process data file and convert format""" + pass +``` + +**Complete API Call:** + +```json +{ + "path": "f/data/process_file", + "summary": "Process data file and convert format", + "description": "Reads input file, converts to specified format, optionally compresses", + "language": "python3", + "content": "def main(input_file: str, output_format: str = \"json\", compress: bool = False, max_records: int = 1000):\n \"\"\"Process data file and convert format\"\"\"\n pass", + "schema": { + "type": "object", + "required": ["input_file"], + "properties": { + "input_file": { + "type": "string", + "description": "Path to input file to process" + }, + "output_format": { + "type": "string", + "description": "Output format (json, csv, xml)", + "enum": ["json", "csv", "xml"], + "default": "json" + }, + "compress": { + "type": "boolean", + "description": "Enable gzip compression", + "default": false + }, + "max_records": { + "type": "integer", + "description": "Maximum records to process", + "default": 1000 + } + } + } +} +``` + +### Example 2: API Integration Script + +**TypeScript Code:** + +```typescript +export async function main( + api_key: string, + endpoint: string, + method: "GET" | "POST" = "GET", + payload?: object, +) { + // Make API call +} +``` + +**Complete API Call:** + +```json +{ + "path": "f/integrations/api_call", + "summary": "Make HTTP API call", + "language": "deno", + "content": "export async function main(api_key: string, endpoint: string, method: \"GET\" | \"POST\" = \"GET\", payload?: object) {\n // Implementation\n}", + "schema": { + "type": "object", + "required": ["api_key", "endpoint"], + "properties": { + "api_key": { + "type": "string", + "description": "API authentication key" + }, + "endpoint": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "description": "HTTP method", + "enum": ["GET", "POST"], + "default": "GET" + }, + "payload": { + "type": "object", + "description": "Request payload (for POST)", + "properties": {} + } + } + } +} +``` + +### Example 3: Database Query Script + +**Python with Resource Type:** + +```python +from typing import TypedDict + +class PostgresResource(TypedDict): + host: str + port: int + user: str + password: str + database: str + +def main(db: PostgresResource, query: str, limit: int = 100): + """Execute database query""" + pass +``` + +**Complete API Call:** + +```json +{ + "path": "f/database/execute_query", + "summary": "Execute database query", + "language": "python3", + "content": "def main(db, query: str, limit: int = 100):\n \"\"\"Execute database query\"\"\"\n pass", + "schema": { + "type": "object", + "required": ["db", "query"], + "properties": { + "db": { + "type": "object", + "description": "PostgreSQL database connection", + "format": "resource-postgresql" + }, + "query": { + "type": "string", + "description": "SQL query to execute" + }, + "limit": { + "type": "integer", + "description": "Maximum rows to return", + "default": 100 + } + } + } +} +``` + +## Additional Resources + +- **Windmill Scripts Guide**: See [windmill-scripts-guide.md](./windmill-scripts-guide.md) for comprehensive script creation documentation +- **Official JSON Schema Spec**: [json-schema.org](https://json-schema.org/) +- **Windmill Script Parameters**: [Windmill Docs - Script Parameters](https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#parameters) +- **Windmill Resource Types**: [Windmill Docs - Resources](https://www.windmill.dev/docs/core_concepts/resources_and_types) +- **Windmill Advanced Parameters**: [Windmill Docs - Advanced Parameters](https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing) + +## Quick Reference Card + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ JSON SCHEMA QUICK REFERENCE โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Basic Structure: โ”‚ +โ”‚ { โ”‚ +โ”‚ "type": "object", โ”‚ +โ”‚ "required": ["param1"], โ”‚ +โ”‚ "properties": { โ”‚ +โ”‚ "param1": {"type": "string"} โ”‚ +โ”‚ } โ”‚ +โ”‚ } โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Common Types: โ”‚ +โ”‚ "string" | "integer" | "number" | "boolean" โ”‚ +โ”‚ "array" | "object" | "null" โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Features: โ”‚ +โ”‚ "default": value - Set default value โ”‚ +โ”‚ "enum": [...] - Fixed choices (dropdown) โ”‚ +โ”‚ "format": "resource-" - Windmill resource type โ”‚ +โ”‚ "description": "..." - Help text in UI โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Remember: API requires manual schemas, UI does not! โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +**Last Updated**: 2025-11-18 +**Related Guides**: [windmill-scripts-guide.md](./windmill-scripts-guide.md) diff --git a/docs/testing-setup-guide.md b/docs/testing-setup-guide.md new file mode 100644 index 0000000..776b628 --- /dev/null +++ b/docs/testing-setup-guide.md @@ -0,0 +1,403 @@ +# Testing Setup Guide + +This guide clarifies the test configuration and setup for the Windmill MCP Server project. + +## Overview + +There are **two different ways** to use the Windmill MCP server: + +1. **OpenCode Agents** - Using the MCP server through OpenCode's agent system +2. **Direct E2E Tests** - Testing the MCP server programmatically with test scripts + +Each has slightly different configuration requirements. + +--- + +## Configuration Files + +### 1. `.opencode/opencode.jsonc` (OpenCode Agents) + +**Purpose**: Configures how OpenCode agents access the Windmill MCP server + +**Location**: `/Users/nroth/workspace/windmill-mcp/.opencode/opencode.jsonc` + +**Current Configuration**: + +```json +{ + "mcp": { + "windmill-dev": { + "type": "local", + "command": ["node", "./build/dist/index.js"], + "environment": { + "WINDMILL_BASE_URL": "http://localhost:8000", + "WINDMILL_API_TOKEN": "test-super-secret" + }, + "enabled": true + } + }, + "agent": { + "build": { "tools": { "windmill-dev_*": false } }, + "general": { "tools": { "windmill-dev_*": false } }, + "plan": { "tools": { "windmill-dev_*": false } } + } +} +``` + +**Key Points**: + +- Server name is `windmill-dev` +- Tools are prefixed with `windmill-dev_` when accessed by agents +- Example: `workspace_list_listWorkspaces` becomes `windmill-dev_workspace_list_listWorkspaces` +- Command points to **compiled JavaScript**: `./build/dist/index.js` (NOT TypeScript!) +- Uses Docker dev server credentials by default + +### 2. `tests/setup.js` (E2E Test Defaults) + +**Purpose**: Sets default environment variables for E2E tests + +**Default Values**: + +```javascript +process.env.WINDMILL_BASE_URL = "http://localhost:8000"; +process.env.WINDMILL_API_TOKEN = "test-super-secret"; // Docker superadmin secret +process.env.E2E_WORKSPACE = "admins"; // Default workspace +``` + +### 3. `tests/config.json` (Test Configuration) + +**Purpose**: Test suite configuration + +**Current Values**: + +```json +{ + "windmill": { + "baseUrl": "http://localhost:8000", + "workspace": "admins", + "timeout": 30000, + "superadminSecret": "test-super-secret" + } +} +``` + +--- + +## Tool Name Resolution + +### How Tool Names Work + +1. **In Generated Code** (`build/src/index.ts`): + + ```typescript + name: "workspace_list_listWorkspaces"; + ``` + +2. **When Used by OpenCode Agents**: + + ``` + windmill-dev_workspace_list_listWorkspaces + ``` + + The `windmill-dev_` prefix comes from the MCP server name in `opencode.jsonc` + +3. **When Used in E2E Tests**: + ```javascript + name: "listWorkspaces"; // Original OpenAPI operationId + ``` + Tests can use either the namespaced version or map back to the original + +### Agent Tool Access Patterns + +In `.opencode/agent/*.md` files, agents are configured with glob patterns: + +```yaml +tools: + "*": false + "windmill-dev_workspace_*": true +``` + +This grants access to all workspace-related tools from the `windmill-dev` MCP server: + +- `windmill-dev_workspace_list_listWorkspaces` +- `windmill-dev_workspace_manage_createWorkspace` +- etc. + +--- + +## Setup for Different Use Cases + +### 1. OpenCode Agents (Development) + +**Goal**: Enable OpenCode agents to interact with local Windmill instance + +**Steps**: + +```bash +# 1. Start Windmill +npm run docker:dev + +# 2. Generate and build MCP server +npm run generate + +# 3. Verify build completed +ls -lh build/dist/index.js + +# 4. Restart OpenCode to pick up config changes +# (or reload window if using VS Code extension) +``` + +**Configuration Check**: + +- `.opencode/opencode.jsonc` should have `windmill-dev` server +- Command should be `["node", "./build/dist/index.js"]` +- Environment should have Windmill URL and token +- `enabled: true` + +**Troubleshooting**: +If agents report tools not found: + +1. Check `build/dist/index.js` exists +2. Verify `.opencode/opencode.jsonc` has correct path +3. Restart OpenCode to reload configuration +4. Check Windmill is running: `curl http://localhost:8000/api/version` + +### 2. E2E Tests (Automated Testing) + +**Goal**: Run automated tests against MCP server + +**Steps**: + +```bash +# Full E2E test cycle (automated) +npm run test:e2e:full + +# Or manual steps: +npm run docker:dev # Start Windmill +npm run generate # Generate MCP server +npm run test:e2e # Run E2E tests +``` + +**Environment Variables**: + +```bash +# Optional - defaults are set in tests/setup.js +export E2E_WINDMILL_URL=http://localhost:8000 +export E2E_WINDMILL_TOKEN=test-super-secret +export E2E_WORKSPACE=admins +``` + +**Test Files**: + +- `tests/e2e/windmill-api.test.js` - Direct Windmill API tests +- `tests/e2e/mcp-integration.test.js` - MCP server integration tests + +### 3. Production Testing + +**Goal**: Test against a real Windmill instance + +**Steps**: + +```bash +# 1. Create user token in Windmill UI +# Settings โ†’ Tokens โ†’ Create Token + +# 2. Set environment variables +export E2E_WINDMILL_URL=https://your-instance.windmill.dev +export E2E_WINDMILL_TOKEN=your-user-token +export E2E_WORKSPACE=your-workspace + +# 3. Update .opencode/opencode.jsonc +{ + "mcp": { + "windmill-dev": { + "environment": { + "WINDMILL_BASE_URL": "https://your-instance.windmill.dev", + "WINDMILL_API_TOKEN": "your-user-token" + } + } + } +} + +# 4. Run tests +npm run test:e2e +``` + +--- + +## Common Issues and Solutions + +### Issue 1: "Tools not found" in OpenCode + +**Symptom**: Agents report `windmill-dev_workspace_list_listWorkspaces` not found + +**Causes**: + +1. MCP server not generated/built +2. Wrong path in `.opencode/opencode.jsonc` +3. OpenCode hasn't reloaded configuration +4. Windmill not running + +**Solutions**: + +```bash +# Verify build exists +ls -lh build/dist/index.js + +# Rebuild if needed +npm run generate + +# Check OpenCode config has correct path +cat .opencode/opencode.jsonc | grep command + +# Restart OpenCode +``` + +### Issue 2: E2E Tests Fail with 401 Unauthorized + +**Symptom**: Tests fail with authentication errors + +**Causes**: + +1. Wrong API token +2. Token expired +3. Windmill not running + +**Solutions**: + +```bash +# Check Windmill is running +curl http://localhost:8000/api/version + +# Verify token works +curl -H "Authorization: Bearer test-super-secret" \ + http://localhost:8000/api/version + +# Check environment variables +echo $E2E_WINDMILL_TOKEN +``` + +### Issue 3: TypeScript Build Failed + +**Symptom**: `build/dist/index.js` doesn't exist + +**Solution**: + +```bash +# Rebuild +cd build +npm install +npm run build + +# Or regenerate everything +cd .. +npm run generate +``` + +### Issue 4: Wrong Server Path in Tests + +**Symptom**: E2E tests fail to start MCP server + +**Location**: `tests/e2e/mcp-integration.test.js:36` + +**Check**: + +```javascript +const serverPath = path.join(__dirname, "../../build/dist/index.js"); +``` + +Should point to the compiled JavaScript, not TypeScript source. + +--- + +## File Structure Reference + +``` +windmill-mcp/ +โ”œโ”€โ”€ .opencode/ +โ”‚ โ”œโ”€โ”€ opencode.jsonc # OpenCode MCP config +โ”‚ โ””โ”€โ”€ agent/ # Agent configurations +โ”‚ โ”œโ”€โ”€ windmill-manager.md +โ”‚ โ””โ”€โ”€ *-specialist.md # tools: "windmill-dev_*": true +โ”œโ”€โ”€ build/ +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Generated TypeScript (source) +โ”‚ โ””โ”€โ”€ build/ +โ”‚ โ””โ”€โ”€ index.js # Compiled JavaScript (executable) +โ”œโ”€โ”€ tests/ +โ”‚ โ”œโ”€โ”€ setup.js # Test defaults +โ”‚ โ”œโ”€โ”€ config.json # Test configuration +โ”‚ โ””โ”€โ”€ e2e/ +โ”‚ โ”œโ”€โ”€ windmill-api.test.js +โ”‚ โ””โ”€โ”€ mcp-integration.test.js +โ””โ”€โ”€ docs/ + โ””โ”€โ”€ testing-setup-guide.md # This file +``` + +--- + +## Verification Checklist + +Before running tests or using agents, verify: + +- [ ] Windmill running: `curl http://localhost:8000/api/version` +- [ ] MCP server built: `ls build/dist/index.js` +- [ ] OpenCode config correct: `.opencode/opencode.jsonc` โ†’ `build/dist/index.js` +- [ ] Environment variables set (if needed) +- [ ] OpenCode reloaded (if changed config) + +--- + +## Quick Reference + +### Start Everything + +```bash +# Terminal 1: Start Windmill +npm run docker:dev + +# Terminal 2: Generate MCP server +npm run generate + +# Terminal 3: Run tests +npm run test:e2e + +# For OpenCode agents: Just restart OpenCode +``` + +### Stop Everything + +```bash +# Stop Windmill +npm run docker:down + +# Clean slate (removes all data) +npm run docker:clean +``` + +### Check Status + +```bash +# Windmill +curl http://localhost:8000/api/version + +# MCP server build +ls -lh build/dist/index.js + +# Test with MCP server directly +node build/dist/index.js +``` + +--- + +## Summary + +**Key Takeaways**: + +1. **OpenCode agents** need `.opencode/opencode.jsonc` configured with correct path to `build/dist/index.js` +2. **Tool names** are prefixed with MCP server name: `windmill-dev_workspace_*` +3. **E2E tests** use environment variables or defaults from `tests/setup.js` +4. **Docker dev setup** provides instant credentials: `test-super-secret` / workspace `admins` +5. **Always rebuild** after changing generation: `npm run generate` + +For questions or issues, check the main testing guide: [docs/testing.md](./testing.md) diff --git a/docs/windmill-agent-team-plan.md b/docs/windmill-agent-team-plan.md new file mode 100644 index 0000000..55a152e --- /dev/null +++ b/docs/windmill-agent-team-plan.md @@ -0,0 +1,417 @@ +# Windmill Agent Team Plan + +> **Planning Document** +> +> This document outlines the planned team of AI agents for managing Windmill instances. It defines roles, responsibilities, tool access, and collaboration patterns. + +## Overview + +The Windmill Agent Team consists of a primary **Windmill Manager** agent that orchestrates tasks and delegates to specialized **subagents**. Each subagent focuses on a specific domain of Windmill functionality, enabling efficient parallel processing and expertise-based task handling. + +## Architecture + +### Primary Agent + +- **Windmill Manager**: Central coordinator with high-level oversight + - Receives user requests + - Analyzes requirements + - Delegates tasks to appropriate subagents + - Synthesizes results + - Has access to informational tools across all domains + +### Subagents + +Specialized agents for specific Windmill domains: + +- Job Specialist +- User Specialist +- Script Specialist +- Flow Specialist +- Resource Specialist +- Trigger Specialist +- App Specialist +- Workspace Specialist +- Audit Specialist +- Integration Specialist +- Storage Specialist +- System Specialist + +## Role Definitions + +Each role follows the template structure in `.opencode/templates/agent/windmill-ROLE.md`. + +### Windmill Manager + +**File**: `.opencode/agent/windmill-manager.md` + +```yaml +--- +name: "windmill-manager" +description: "Central coordinator for Windmill management tasks - orchestrates subagents and provides high-level oversight" +mode: "primary" +model: "github-copilot/grok-code-fast-1" +tools: + "*": false # Disable all by default + "windmill-dev_settings_system_backendVersion": true # Example explicit tool +--- +``` + +**Responsibilities**: + +- Analyze incoming requests +- Break down complex tasks into subtasks +- Assign work to appropriate subagents +- Monitor progress and synthesize results +- Handle cross-domain coordination + +**Instructions**: + +- Always delegate specialized work to subagents +- Maintain overview of all active tasks +- Ensure consistent communication between agents +- Provide final summaries to users + +### Job Specialist + +**File**: `.opencode/agent/job-specialist.md` + +```yaml +--- +name: "job-specialist" +description: "Handles job execution, monitoring, and management" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_job_*": true +--- +``` + +**Responsibilities**: + +- Job execution and monitoring +- Queue management +- Job cancellation and resumption +- Performance analysis +- Error handling and retries + +### User Specialist + +**File**: `.opencode/agent/user-specialist.md` + +```yaml +--- +name: "user-specialist" +description: "Manages user accounts, authentication, and permissions" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_user_*": true +--- +``` + +**Responsibilities**: + +- User account management +- Authentication and authorization +- Group and role management +- Invite and access control +- Token management + +### Script Specialist + +**File**: `.opencode/agent/script-specialist.md` + +```yaml +--- +name: "script-specialist" +description: "Manages scripts, deployment, and execution" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_script_*": true +--- +``` + +**Responsibilities**: + +- Script creation and editing +- Deployment management +- Version control +- Hub integration +- Script execution coordination + +### Flow Specialist + +**File**: `.opencode/agent/flow-specialist.md` + +```yaml +--- +name: "flow-specialist" +description: "Manages workflows and flow orchestration" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_flow_*": true +--- +``` + +**Responsibilities**: + +- Flow design and modification +- Step orchestration +- Flow debugging +- Performance optimization +- Hub integration + +### Resource Specialist + +**File**: `.opencode/agent/resource-specialist.md` + +```yaml +--- +name: "resource-specialist" +description: "Manages resources and resource types" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_resource_*": true +--- +``` + +**Responsibilities**: + +- Resource creation and management +- Resource type definitions +- Connection management +- Resource value interpolation + +### Trigger Specialist + +**File**: `.opencode/agent/trigger-specialist.md` + +```yaml +--- +name: "trigger-specialist" +description: "Manages triggers and event-driven automation" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_trigger_*": true +--- +``` + +**Responsibilities**: + +- Trigger configuration +- Event source integration +- Webhook management +- Schedule management +- Connection testing + +### App Specialist + +**File**: `.opencode/agent/app-specialist.md` + +```yaml +--- +name: "app-specialist" +description: "Manages apps and user interfaces" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_app_*": true +--- +``` + +**Responsibilities**: + +- App creation and editing +- UI component management +- App deployment +- Version control +- Hub integration + +### Workspace Specialist + +**File**: `.opencode/agent/workspace-specialist.md` + +```yaml +--- +name: "workspace-specialist" +description: "Manages workspaces and multi-tenancy" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_workspace_*": true +--- +``` + +**Responsibilities**: + +- Workspace creation and management +- User assignment +- Settings configuration +- Usage monitoring +- Security policies + +### Audit Specialist + +**File**: `.opencode/agent/audit-specialist.md` + +```yaml +--- +name: "audit-specialist" +description: "Handles auditing, logging, and compliance" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_audit_*": true +--- +``` + +**Responsibilities**: + +- Audit log analysis +- Security monitoring +- Compliance reporting +- Log aggregation and search + +### Integration Specialist + +**File**: `.opencode/agent/integration-specialist.md` + +```yaml +--- +name: "integration-specialist" +description: "Manages external integrations and connections" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_integration_*": true +--- +``` + +**Responsibilities**: + +- OAuth configuration +- Slack/Teams integration +- Webhook setup +- External API connections +- Authentication flows + +### Storage Specialist + +**File**: `.opencode/agent/storage-specialist.md` + +```yaml +--- +name: "storage-specialist" +description: "Manages data storage and databases" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_storage_*": true +--- +``` + +**Responsibilities**: + +- Database connections +- File storage management +- S3 integration +- Data preview and loading +- Storage optimization + +### System Specialist + +**File**: `.opencode/agent/system-specialist.md` + +```yaml +--- +name: "system-specialist" +description: "Handles system administration and configuration" +mode: "subagent" +model: "github-copilot/grok-code-fast-1" +tools: + "windmill-dev_*": false + "windmill-dev_system_*": true + "windmill-dev_settings_*": true +--- +``` + +**Responsibilities**: + +- System health monitoring +- Configuration management +- License management +- Performance tuning +- Critical alerts + +## Workflow Patterns + +### Task Decomposition + +1. **User Request** โ†’ Windmill Manager +2. **Analysis** โ†’ Identify required domains +3. **Delegation** โ†’ Create subtasks for relevant specialists +4. **Parallel Execution** โ†’ Subagents work simultaneously +5. **Synthesis** โ†’ Manager combines results +6. **Response** โ†’ Final answer to user + +### Communication + +- Subagents communicate through shared context +- Manager maintains task status +- Results aggregated by domain +- Cross-domain dependencies handled by manager + +### Error Handling + +- Subagents report failures to manager +- Manager can reassign or escalate +- Rollback coordination when needed + +## Tool Access Matrix + +| Role | Tool Groups | Access Level | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------- | +| Windmill Manager | `windmill-dev_*_list`, `windmill-dev_*_hub`, `windmill-dev_system_*`, `windmill-dev_settings_*`, `windmill-dev_audit_*` | Informational | +| Job Specialist | `windmill-dev_job_*` | Domain-specific | +| User Specialist | `windmill-dev_user_*` | Domain-specific | +| Script Specialist | `windmill-dev_script_*` | Domain-specific | +| Flow Specialist | `windmill-dev_flow_*` | Domain-specific | +| Resource Specialist | `windmill-dev_resource_*` | Domain-specific | +| Trigger Specialist | `windmill-dev_trigger_*` | Domain-specific | +| App Specialist | `windmill-dev_app_*` | Domain-specific | +| Workspace Specialist | `windmill-dev_workspace_*` | Domain-specific | +| Audit Specialist | `windmill-dev_audit_*` | Domain-specific | +| Integration Specialist | `windmill-dev_integration_*` | Domain-specific | +| Storage Specialist | `windmill-dev_storage_*` | Domain-specific | +| System Specialist | `windmill-dev_system_*`, `windmill-dev_settings_*` | Multi-domain | + +## Implementation Notes + +- All agents use the same model for consistency +- Tool access controlled via glob patterns +- Subagents have focused responsibilities to prevent overlap +- Manager has orchestration tools for coordination +- Roles can be extended or combined as needed + +## Next Steps + +1. Create agent configuration files in `.opencode/agent/` +2. Test individual agent capabilities +3. Implement manager-subagent communication +4. Validate tool access controls +5. Document usage patterns diff --git a/docs/windmill-scripts-guide.md b/docs/windmill-scripts-guide.md new file mode 100644 index 0000000..6c56982 --- /dev/null +++ b/docs/windmill-scripts-guide.md @@ -0,0 +1,401 @@ +# Windmill Scripts Guide + +This guide provides a concise reference for working with Windmill scripts via the MCP API. + +## Core Concepts + +### What is a Windmill Script? + +Scripts are the foundation of Windmill and can be written in multiple languages: + +- TypeScript (Deno, Bun, Node.js) +- Python +- Go +- Bash/PowerShell/Nu +- PHP +- SQL (PostgreSQL, MySQL, MS SQL, BigQuery, Snowflake) +- REST/GraphQL +- Docker, Rust, Ansible, C#, Java, Ruby + +**๐Ÿ“– [Official Script Editor Docs](https://www.windmill.dev/docs/script_editor)** + +### Main Function Requirement + +Every script must have a **main function** that serves as the entrypoint: + +**TypeScript:** + +```typescript +async function main(param1: string, param2: { nested: string }) { + // Your code here +} +``` + +**Python:** + +```python +def main(param1: str, param2: dict): + # Your code here +``` + +**Go:** + +```go +func main(x string, nested struct{ Foo string `json:"foo"` }) (interface{}, error) { + // Your code here +} +``` + +**Bash:** +No main function needed - body is executed directly with args passed. + +## JSON Schema & Input Types + +โš ๏ธ **CRITICAL FOR API/MCP USERS**: When creating scripts via API or MCP tools, you **MUST** manually provide JSON Schema. See **[JSON Schema Manual Guide](./json-schema-manual-guide.md)** for complete instructions, examples, and common mistakes to avoid. + +Windmill uses [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/schema) to define script inputs and generate auto-UIs. + +**๐Ÿ“– [JSON Schema Documentation](https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing)** + +### Type Mapping + +#### Python Types โ†’ JSON Schema + +| Python | JSON Schema | +| ------------------- | --------------------------- | +| `str` | `string` | +| `float` | `number` | +| `int` | `integer` | +| `bool` | `boolean` | +| `dict` | `object` | +| `list` | `any[]` | +| `List[str]` | `string[]` | +| `bytes` | `string` (base64) | +| `datetime` | `string` (date-time format) | +| `Literal["a", "b"]` | `string` with enums | +| `DynSelect_foo` | Dynamic select dropdown | + +#### TypeScript Types โ†’ JSON Schema + +| TypeScript | JSON Schema | +| ---------------- | ------------------------------------- | +| `string` | `string` | +| `number` | `number` | +| `bigint` | `int` | +| `boolean` | `boolean` | +| `object` | `object` | +| `string[]` | `string[]` | +| `"a" \| "b"` | `string` with enums | +| `wmill.Base64` | `string` (base64 encoding) | +| `wmill.Email` | `string` (email format) | +| `wmill.Sql` | `string` (SQL format - Monaco editor) | +| `` | `object` (resource format) | +| `DynSelect_foo` | Dynamic select dropdown | + +### Resource Types + +Reference workspace resources by type (CamelCase โ†’ snake_case): + +**TypeScript:** + +```typescript +type Postgresql = object; // References 'postgresql' resource type + +export async function main(db: Postgresql) { + // db contains resource configuration +} +``` + +**Python:** + +```python +postgresql = dict + +def main(db: postgresql): + # db contains resource configuration + pass +``` + +**๐Ÿ“– [Resources & Resource Types](https://www.windmill.dev/docs/core_concepts/resources_and_types)** + +### Advanced Input Features + +#### Dynamic Select + +Create dropdowns with dynamically computed options based on other inputs: + +**TypeScript:** + +```typescript +export type DynSelect_category = string; + +export async function category(department: string, region: string) { + if (department === "sales") { + return [ + { value: "enterprise", label: "Enterprise Sales" }, + { value: "smb", label: "SMB Sales" }, + ]; + } + return [{ value: "general", label: "General" }]; +} + +export async function main( + department: string, + region: string, + category: DynSelect_category, +) { + console.log(category); +} +``` + +**Python:** + +```python +DynSelect_category = str + +def category(department: str, region: str): + if department == "sales": + return [ + {"value": "enterprise", "label": "Enterprise Sales"}, + {"value": "smb", "label": "SMB Sales"} + ] + return [{"value": "general", "label": "General"}] + +def main(department: str, region: str, category: DynSelect_category): + print(category) +``` + +#### oneOf (Union Types) + +Allow users to pick between different object shapes: + +**TypeScript:** + +```typescript +type Config = + | { label: "Database"; connection_string: string } + | { label: "API"; api_key: string; endpoint: string }; + +export async function main(config: Config) { + if (config.label === "Database") { + // config.connection_string is available + } else { + // config.api_key and config.endpoint are available + } +} +``` + +The UI will render a dropdown to select which variant, then show fields for that variant. + +### Input Field Advanced Settings + +When creating or editing scripts, each argument supports advanced configuration: + +- **String**: Min/max length, textarea rows, password (creates variable), format (email, uri, uuid, ipv4, yaml, sql, date-time), pattern (regex), enum +- **Integer/Number**: Min/max, currency, locale +- **Object**: Resource type selection +- **Array**: Item types (strings, enums, objects, numbers, bytes) +- **Boolean**: No advanced settings +- **Any**: No advanced settings + +**๐Ÿ“– [Customize Generated UI](https://www.windmill.dev/docs/script_editor/customize_ui)** + +## Creating Scripts via API + +When creating scripts through the Windmill MCP API, you need to provide: + +1. **Path**: Unique identifier (e.g., `u/username/script_name` or `f/folder/script_name`) +2. **Content**: Script code with main function +3. **Language**: Script language (python3, deno, go, bash, etc.) +4. **Schema**: JSON Schema defining inputs (optional - can be inferred) +5. **Description**: What the script does +6. **Summary**: Short description + +### Example: Creating a Python Script + +```json +{ + "path": "u/admin/hello_world", + "summary": "Greet a user", + "description": "Takes a name and returns a greeting", + "content": "def main(name: str):\n return f'Hello, {name}!'", + "language": "python3", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name to greet" + } + }, + "required": ["name"] + } +} +``` + +### Example: Creating a TypeScript Script with Resource + +```json +{ + "path": "f/integrations/query_database", + "summary": "Query PostgreSQL database", + "description": "Executes a SQL query against a PostgreSQL database", + "content": "type Postgresql = object;\n\nexport async function main(db: Postgresql, query: string) {\n // Query logic here\n return { rows: [] };\n}", + "language": "deno", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "db": { + "type": "object", + "format": "resource-postgresql", + "description": "PostgreSQL database connection" + }, + "query": { + "type": "string", + "format": "sql", + "description": "SQL query to execute" + } + }, + "required": ["db", "query"] + } +} +``` + +## Script Metadata & Settings + +Scripts have configurable metadata: + +- **Summary**: Short description shown in lists +- **Description**: Detailed explanation +- **Is Template**: Mark as reusable template +- **Kind**: Normal script, approval script, trigger, etc. +- **Concurrency Limits**: Max concurrent executions +- **Dedicated Workers**: Assign to specific worker tags +- **Cache TTL**: Cache results for period +- **Timeout**: Max execution time +- **Delete After Use**: Auto-delete after run +- **Restart Unless Cancelled**: Perpetual script behavior + +**๐Ÿ“– [Script Settings](https://www.windmill.dev/docs/script_editor/settings)** +**๐Ÿ“– [Script Kinds](https://www.windmill.dev/docs/script_editor/script_kinds)** +**๐Ÿ“– [Concurrency Limits](https://www.windmill.dev/docs/script_editor/concurrency_limit)** +**๐Ÿ“– [Perpetual Scripts](https://www.windmill.dev/docs/script_editor/perpetual_scripts)** + +## Running Scripts + +Scripts can be triggered via: + +1. **Direct API call**: `runScriptByPath`, `runScriptByHash` +2. **Webhooks**: HTTP endpoints for external triggers +3. **Schedules**: Cron-based execution +4. **Flow steps**: Part of a workflow +5. **App components**: Triggered by UI interactions + +**๐Ÿ“– [Triggers](https://www.windmill.dev/docs/getting_started/triggers)** +**๐Ÿ“– [Webhooks](https://www.windmill.dev/docs/core_concepts/webhooks)** +**๐Ÿ“– [Schedules](https://www.windmill.dev/docs/core_concepts/scheduling)** + +## Script Versioning + +Windmill automatically versions scripts: + +- Each deployment creates a new version identified by hash +- Can view/restore previous versions +- Deployment history tracked +- Supports draft โ†’ deploy workflow + +**๐Ÿ“– [Versioning](https://www.windmill.dev/docs/core_concepts/versioning)** +**๐Ÿ“– [Draft & Deploy](https://www.windmill.dev/docs/core_concepts/draft_and_deploy)** + +## Dependencies & Imports + +### Python + +Uses `requirements.txt` or inline imports. Windmill auto-generates lockfile. + +```python +# Inline import +import requests +import pandas as pd + +def main(): + pass +``` + +### TypeScript (Deno) + +Uses npm: prefix or Deno imports: + +```typescript +import axios from "npm:axios@1.6.0"; +import { S3Client } from "https://deno.land/x/s3_lite_client@0.2.0/mod.ts"; + +export async function main() {} +``` + +**๐Ÿ“– [Dependency Management](https://www.windmill.dev/docs/advanced/imports)** + +## Backend Schema Validation + +Add `schema_validation` comment to enforce strict input validation: + +**TypeScript:** + +```typescript +// schema_validation +export async function main(count: number, status: "active" | "inactive") { + // Will fail if count is not a number or status is not 'active'/'inactive' +} +``` + +**Python:** + +```python +# schema_validation +def main(count: int, status: Literal["active", "inactive"]): + # Will fail if types don't match + pass +``` + +## Best Practices + +1. **Keep scripts focused**: One clear purpose per script (avoid 1000+ line scripts) +2. **Use flows for complexity**: Chain scripts together for multi-step processes +3. **Leverage resources**: Store credentials/configs as workspace resources +4. **Add descriptions**: Help others understand inputs and behavior +5. **Use Hub scripts**: Browse [Windmill Hub](https://hub.windmill.dev) for reusable scripts +6. **Version control**: Use Git sync or CLI sync for production scripts +7. **Test thoroughly**: Use instant preview before deployment +8. **Handle errors**: Implement proper error handling and retries +9. **Document parameters**: Clear descriptions for all inputs +10. **Share logic**: Extract common code to shared modules + +**๐Ÿ“– [Workflows as Code](https://www.windmill.dev/docs/core_concepts/workflows_as_code)** +**๐Ÿ“– [Sharing Common Logic](https://www.windmill.dev/docs/advanced/sharing_common_logic)** +**๐Ÿ“– [Error Handling](https://www.windmill.dev/docs/core_concepts/error_handling)** + +## Quick Reference + +| Task | API Tool/Method | +| ---------------------- | ------------------------------------ | +| Create script | `createScript` | +| Update script | `updateScript` | +| Get script by path | `getScriptByPath` | +| List workspace scripts | `listScripts` | +| Run script | `runScriptByPath`, `runScriptByHash` | +| Delete script | `deleteScript` | +| Archive script | `archiveScriptByPath` | +| Get Hub script | `getHubScript` | +| Search Hub | `queryHubScripts` | + +## Additional Resources + +- **[Auto-generated UIs](https://www.windmill.dev/docs/core_concepts/auto_generated_uis)** - How Windmill creates UIs from schemas +- **[Instant Preview](https://www.windmill.dev/docs/core_concepts/instant_preview)** - Test scripts immediately +- **[Code Editor](https://www.windmill.dev/docs/code_editor)** - Editor features and shortcuts +- **[Local Development](https://www.windmill.dev/docs/advanced/local_development)** - Develop scripts locally +- **[VS Code Extension](https://www.windmill.dev/docs/script_editor/vs_code_scripts)** - Run scripts in VS Code +- **[Windmill Hub](https://hub.windmill.dev)** - Browse & share scripts +- **[API Documentation](https://app.windmill.dev/openapi.html)** - Complete OpenAPI spec diff --git a/package-lock.json b/package-lock.json index 25b5cc5..69458bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", + "@modelcontextprotocol/sdk": "^1.10.0", "dotenv": "^16.4.5", "tar": "^7.0.0" }, @@ -728,16 +728,59 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", - "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz", + "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==", "license": "MIT", "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@mswjs/interceptors": { "version": "0.40.0", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", @@ -1322,6 +1365,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1375,6 +1431,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1432,6 +1527,38 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1475,6 +1602,35 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1633,6 +1789,19 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -1652,11 +1821,32 @@ "node": ">=18" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1671,7 +1861,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1749,6 +1938,26 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1756,6 +1965,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1769,6 +1987,36 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1818,6 +2066,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1992,6 +2246,36 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -2016,11 +2300,76 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2067,6 +2416,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2110,6 +2475,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2149,6 +2531,24 @@ "dev": true, "license": "ISC" }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2171,6 +2571,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2191,6 +2600,43 @@ "node": "*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -2255,6 +2701,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2289,6 +2747,30 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -2418,6 +2900,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2478,6 +2969,12 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -2495,7 +2992,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/js-tokens": { @@ -2742,6 +3238,15 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -2749,6 +3254,27 @@ "dev": true, "license": "MIT" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2780,6 +3306,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -2874,7 +3421,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -2971,6 +3517,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -3000,11 +3555,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3096,6 +3683,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3120,7 +3716,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3170,6 +3765,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -3272,6 +3876,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3292,6 +3909,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3313,6 +3945,15 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", @@ -3345,6 +3986,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", @@ -3442,6 +4092,32 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3472,6 +4148,43 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3482,7 +4195,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3495,12 +4207,83 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3557,7 +4340,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -3808,6 +4590,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -3865,6 +4661,15 @@ "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -4018,7 +4823,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4076,7 +4880,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xmlcreate": { @@ -4168,6 +4971,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 743f885..003ca4d 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "scripts": { "generate": "node src/generator/generate.js", "fetch-spec": "node src/generator/fetch-spec.js", + "generate-tool-list": "node src/generator/generate-tool-list.js", "build:generated": "cd build && npm install && npm run build", "pregenerate": "npm run fetch-spec", - "postgenerate": "npm run apply-overrides && npm run build:generated", + "postgenerate": "npm run add-tool-namespaces && npm run apply-overrides && npm run build:generated && npm run generate-tool-list", + "add-tool-namespaces": "node src/overrides/add-tool-namespaces.js", "apply-overrides": "node src/overrides/apply-overrides.js", "validate-overrides": "node src/overrides/validate-overrides.js", - "dev": "node build/build/index.js", + "dev": "node build/dist/index.js", + "dev:normalized": "node scripts/normalize-and-run-mcp.js", "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui", @@ -40,7 +43,8 @@ "docker:logs": "docker compose -f tests/docker/docker-compose.yml logs -f", "docker:wait": "node tests/docker/wait-for-windmill.js", "docker:dev": "npm run docker:up && npm run docker:wait && echo '\nโœ… Windmill ready at http://localhost:8000\n Superadmin secret: test-super-secret\n Default workspace: admins\n'", - "docker:clean": "docker compose -f tests/docker/docker-compose.yml down -v" + "docker:clean": "docker compose -f tests/docker/docker-compose.yml down -v", + "inspector": "node scripts/inspector.js" }, "repository": { "type": "git", @@ -61,7 +65,7 @@ }, "homepage": "https://github.com/rothnic/windmill-mcp#readme", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", + "@modelcontextprotocol/sdk": "^1.10.0", "dotenv": "^16.4.5", "tar": "^7.0.0" }, diff --git a/scripts/inspector.js b/scripts/inspector.js new file mode 100644 index 0000000..7bfe1a4 --- /dev/null +++ b/scripts/inspector.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node +import dotenv from "dotenv"; +dotenv.config(); +import { spawn } from "child_process"; +import process from "process"; + +async function checkBaseUrl(url) { + try { + // normalize + const normalized = url.replace(/\/+$/, ""); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const res = await fetch(`${normalized}/api/version`, { + signal: controller.signal, + }); + clearTimeout(timeout); + return res.ok; + } catch (e) { + return false; + } +} + +async function runCmd(command, args, options = {}) { + return new Promise((resolve, reject) => { + const p = spawn(command, args, { stdio: "inherit", ...options }); + p.on("close", (code) => + code === 0 ? resolve(0) : reject(new Error(`Exited with ${code}`)), + ); + p.on("error", (err) => reject(err)); + }); +} + +async function main() { + const env = { ...process.env }; + // Show which token env vars are present (don't log secrets) + const tokenVars = [ + "WINDMILL_API_TOKEN", + "E2E_WINDMILL_TOKEN", + "BEARER_TOKEN_BEARERAUTH", + "API_KEY_COOKIEAUTH", + ]; + const presentTokens = tokenVars.filter((k) => !!env[k]); + console.log( + "Inspector: token env vars present:", + presentTokens.join(", ") || "(none)", + ); + + env.WINDMILL_BASE_URL = env.WINDMILL_BASE_URL || "http://localhost:8000"; + // Map common token env vars into generated-server expected names + env.BEARER_TOKEN_BEARERAUTH = + env.BEARER_TOKEN_BEARERAUTH || + env.WINDMILL_API_TOKEN || + env.E2E_WINDMILL_TOKEN; + env.API_KEY_COOKIEAUTH = + env.API_KEY_COOKIEAUTH || env.WINDMILL_API_TOKEN || env.E2E_WINDMILL_TOKEN; + + let reachable = await checkBaseUrl(env.WINDMILL_BASE_URL); + + // If not reachable and auto-start enabled, attempt to start dev server + const autoStart = + (env.INSPECTOR_AUTO_START_DEV || "true").toLowerCase() !== "false"; + if (!reachable && autoStart) { + console.log( + `\nโš™๏ธ Windmill not reachable at ${env.WINDMILL_BASE_URL}. Attempting to start dev server (npm run docker:dev)...`, + ); + try { + await runCmd("npm", ["run", "docker:dev"], { env }); + } catch (err) { + console.error("Failed to start dev server:", err.message); + process.exit(2); + } + + // Wait for service to become ready (poll) + const maxAttempts = 30; + const delayMs = 2000; + for (let i = 0; i < maxAttempts; i++) { + reachable = await checkBaseUrl(env.WINDMILL_BASE_URL); + if (reachable) break; + console.log(`Waiting for Windmill... (${i + 1}/${maxAttempts})`); + await new Promise((r) => setTimeout(r, delayMs)); + } + } + + if (!reachable) { + console.error( + `\nโŒ Windmill not reachable at ${env.WINDMILL_BASE_URL}/api/version`, + ); + console.error( + "Start the local Windmill (e.g. `npm run docker:dev`) or set WINDMILL_BASE_URL to a reachable instance.\n", + ); + process.exit(2); + } + + const args = [ + "@modelcontextprotocol/inspector", + "node", + "build/dist/index.js", + ]; + console.log(env); + const p = spawn("npx", args, { stdio: "inherit", env }); + p.on("close", (code) => process.exit(code)); + p.on("error", (err) => { + console.error("Failed to run inspector:", err.message); + process.exit(3); + }); +} + +main().catch((err) => { + console.error("Inspector launcher failed:", err.message); + process.exit(4); +}); diff --git a/scripts/normalize-and-run-mcp.js b/scripts/normalize-and-run-mcp.js new file mode 100644 index 0000000..9eeeeb9 --- /dev/null +++ b/scripts/normalize-and-run-mcp.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +// Normalizes legacy WINDMILL env vars to the names generated MCP expects +// and then requires the generated MCP server entrypoint. + +import "dotenv/config"; +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs"; + +const root = process.cwd(); +const entry = path.join(root, "build", "dist", "index.js"); + +// Map of legacy env vars -> expected env var names +const mappings = [ + { + legacy: "WINDMILL_API_TOKEN", + targets: ["BEARER_TOKEN_BEARERAUTH", "API_KEY_COOKIEAUTH"], + }, + { + legacy: "E2E_WINDMILL_TOKEN", + targets: ["BEARER_TOKEN_BEARERAUTH", "API_KEY_COOKIEAUTH"], + }, + { + legacy: "WINDMILL_TOKEN", + targets: ["BEARER_TOKEN_BEARERAUTH", "API_KEY_COOKIEAUTH"], + }, + { + legacy: "TEST_WINDMILL_TOKEN", + targets: ["BEARER_TOKEN_BEARERAUTH", "API_KEY_COOKIEAUTH"], + }, +]; + +for (const m of mappings) { + const v = process.env[m.legacy]; + if (v) { + for (const t of m.targets) { + if (!process.env[t]) process.env[t] = v; + } + } +} + +// If the built entry doesn't exist, try the source runtime as fallback +let runEntry = entry; +if (!fs.existsSync(runEntry)) { + const fallback = path.join(root, "src", "runtime", "index.js"); + if (fs.existsSync(fallback)) runEntry = fallback; +} + +if (!fs.existsSync(runEntry)) { + console.error("Could not find generated MCP entry at", entry); + process.exit(1); +} + +// Spawn node to run the entry so signals and stdio are connected +const child = spawn(process.execPath, [runEntry, ...process.argv.slice(2)], { + stdio: "inherit", + env: process.env, +}); + +child.on("exit", (code, signal) => { + if (signal) process.kill(process.pid, signal); + process.exit(code ?? 0); +}); diff --git a/src/generator/generate-tool-list.js b/src/generator/generate-tool-list.js new file mode 100644 index 0000000..768784a --- /dev/null +++ b/src/generator/generate-tool-list.js @@ -0,0 +1,164 @@ +#!/usr/bin/env node + +/** + * Generate Tool List Documentation + * + * This script generates a markdown document listing all tools + * generated from the Windmill OpenAPI specification. + */ + +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +import { categorizeTool as categorizeToolPattern } from "../utils/tool-categories.js"; + +// Load configuration +const configPath = path.join(__dirname, "config.json"); +let config; +try { + const configData = await fs.readFile(configPath, "utf-8"); + config = JSON.parse(configData); +} catch (error) { + console.error("Error loading config:", error.message); + process.exit(1); +} + +const CACHE_PATH = path.resolve( + __dirname, + "..", + "..", + config.openapi.localCache, +); +const OUTPUT_PATH = path.resolve( + __dirname, + "..", + "..", + "docs/generated-tools.md", +); + +/** + * Group tools by category using regex patterns + */ +function categorizeTool(operationId) { + return categorizeToolPattern(operationId); +} + +/** + * Main function + */ +async function main() { + console.log("๐Ÿ” Generating tool list documentation..."); + + // Read OpenAPI spec + let spec; + try { + const specData = await fs.readFile(CACHE_PATH, "utf-8"); + spec = JSON.parse(specData); + } catch (error) { + console.error("Error reading OpenAPI spec:", error.message); + process.exit(1); + } + + // Extract tools with descriptions + const tools = {}; + for (const [, pathItem] of Object.entries(spec.paths || {})) { + for (const [, operation] of Object.entries(pathItem)) { + if (operation && operation.operationId) { + const category = categorizeTool(operation.operationId); + if (!tools[category]) { + tools[category] = []; + } + tools[category].push({ + operationId: operation.operationId, + description: + operation.summary || + operation.description || + "No description available", + }); + } + } + } + + // Sort categories and tools + const sortedCategories = Object.keys(tools).sort(); + for (const category of sortedCategories) { + tools[category].sort((a, b) => a.operationId.localeCompare(b.operationId)); + } + + // Generate markdown + let markdown = `# Windmill API Tools + +> **โš ๏ธ Auto-generated Document** +> +> This file is automatically generated from the Windmill OpenAPI specification. +> Do not edit this file directly. It will be overwritten during the build process. +> +> Generated on: ${new Date().toISOString()} +> Windmill Version: ${spec.info?.version || "unknown"} + +This document lists all available tools generated from the Windmill API OpenAPI specification. + +`; + + for (const category of sortedCategories) { + markdown += `## ${category}\n\n`; + for (const tool of tools[category]) { + markdown += `### \`${tool.operationId}\`\n\n${tool.description}\n\n`; + } + markdown += `\n`; + } + + markdown += `--- + +Total tools: ${Object.values(tools).reduce((sum, arr) => sum + arr.length, 0)} + +## Tool to Agent Mapping Guide + +This document categorizes all Windmill API tools by functional area. Each category maps to specific OpenCode agents: + +- **job:** โ†’ job-specialist +- **user:** โ†’ user-specialist +- **workspace:** โ†’ workspace-specialist +- **script:** โ†’ script-specialist +- **flow:** โ†’ flow-specialist +- **resource:** โ†’ resource-specialist +- **variable:** โ†’ storage-specialist +- **schedule:** โ†’ trigger-specialist +- **trigger:** โ†’ trigger-specialist +- **app:** โ†’ app-specialist +- **folder:** โ†’ workspace-specialist +- **group:** โ†’ user-specialist +- **storage:** โ†’ storage-specialist +- **integration:** โ†’ integration-specialist +- **audit:** โ†’ audit-specialist +- **settings:** โ†’ system-specialist +- **system:** โ†’ system-specialist +- **capture:** โ†’ system-specialist +- **input:** โ†’ system-specialist +- **draft:** โ†’ system-specialist +- **template:** โ†’ system-specialist +- **tutorial:** โ†’ system-specialist +- **misc:** โ†’ Review category - assign to most relevant specialist + +Use this reference when routing tool requests to the appropriate agent. +`; + + // Write output + try { + await fs.mkdir(path.dirname(OUTPUT_PATH), { recursive: true }); + await fs.writeFile(OUTPUT_PATH, markdown, "utf-8"); + console.log(`โœ… Tool list generated: ${OUTPUT_PATH}`); + } catch (error) { + console.error("Error writing output:", error.message); + process.exit(1); + } +} + +// Run main function +main().catch((error) => { + console.error("Unexpected error:", error); + process.exit(1); +}); diff --git a/src/overrides/add-tool-namespaces.js b/src/overrides/add-tool-namespaces.js new file mode 100644 index 0000000..aa4aae4 --- /dev/null +++ b/src/overrides/add-tool-namespaces.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * Add Tool Namespaces + * + * This script modifies the generated MCP server code to prepend namespace + * prefixes to tool names based on their categorization. + */ + +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const BUILD_INDEX_PATH = path.resolve( + __dirname, + "..", + "..", + "build/src/index.ts", +); + +/** + * Categorize tool based on operationId + * Returns namespace:subgroup format + */ +import { getNamespace } from "../utils/tool-categories.js"; + +/** + * Escape special regex characters + */ +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * Main function + */ +async function main() { + console.log("๐Ÿ”ง Adding tool namespaces..."); + + // Read generated index.ts + let content; + try { + content = await fs.readFile(BUILD_INDEX_PATH, "utf-8"); + } catch (error) { + console.error(`โŒ Error reading ${BUILD_INDEX_PATH}:`, error.message); + process.exit(1); + } + + // Find all tool definitions using regex + // Pattern 1: Map entries like ["operationId", { name: "operationId", ... }] + // Pattern 2: name: "operationId", inside tool definitions + + const mapKeyPattern = /\[\s*["']([^"']+)["']\s*,\s*\{/g; + const toolNamePattern = /name:\s*["']([^"']+)["'],/g; + let match; + const replacements = new Map(); + + // First pass: Find Map keys and their corresponding namespaced names + const mapKeyReplacements = new Map(); + let mapMatch; + while ((mapMatch = mapKeyPattern.exec(content)) !== null) { + const originalKey = mapMatch[1]; + const namespace = getNamespace(originalKey); + const namespacedName = `${namespace}_${originalKey}`; + mapKeyReplacements.set(originalKey, namespacedName); + } + + // Second pass: Replace Map keys + for (const [originalKey, namespacedName] of mapKeyReplacements) { + const oldPattern = `["${originalKey}", {`; + const newPattern = `["${namespacedName}", {`; + content = content.replace( + new RegExp(escapeRegex(oldPattern), "g"), + newPattern, + ); + } + + // Third pass: Replace name properties inside tool definitions + while ((match = toolNamePattern.exec(content)) !== null) { + const originalName = match[1]; + const namespace = getNamespace(originalName); + const namespacedName = `${namespace}_${originalName}`; + replacements.set(match[0], match[0].replace(originalName, namespacedName)); + } + + // Apply name property replacements + let modifiedContent = content; + for (const [oldStr, newStr] of replacements) { + modifiedContent = modifiedContent.replace(oldStr, newStr); + } + + const totalReplacements = mapKeyReplacements.size + replacements.size; + + // Write modified content + try { + await fs.writeFile(BUILD_INDEX_PATH, modifiedContent, "utf-8"); + console.log(`โœ… Added namespaces to ${totalReplacements} tools`); + } catch (error) { + console.error(`โŒ Error writing ${BUILD_INDEX_PATH}:`, error.message); + process.exit(1); + } +} + +// Run main function +main().catch((error) => { + console.error("Unexpected error:", error); + process.exit(1); +}); diff --git a/src/overrides/apply-overrides.js b/src/overrides/apply-overrides.js index db2244f..9b92747 100644 --- a/src/overrides/apply-overrides.js +++ b/src/overrides/apply-overrides.js @@ -78,34 +78,97 @@ async function applyJsonPatch(patchFilePath, srcDir) { console.log(`๐Ÿ”ง Applying JSON patch: ${path.basename(patchFilePath)}`); const patchContent = await fs.readFile(patchFilePath, "utf-8"); - const patches = JSON.parse(patchContent); + let patches; + + // Support both array and object patch formats + try { + patches = JSON.parse(patchContent); + } catch (err) { + console.log(` โŒ Invalid JSON patch file: ${err.message}`); + return 0; + } let appliedCount = 0; - // Process each patch + // If patches is an array (legacy), convert to our object format + if (Array.isArray(patches)) { + // Legacy format: [{ file: "tsconfig.json", operations: [{ type: 'replace', old: '...', new: '...'}]}] + for (const item of patches) { + const target = item.file; + console.log(` โ†’ Processing (legacy): ${target}`); + const fullTargetPath = path.join(srcDir, target); + + // Ensure target file exists + try { + await fs.access(fullTargetPath); + } catch { + console.log(` โš ๏ธ Target file not found: ${target}`); + continue; + } + + let fileContent = await fs.readFile(fullTargetPath, "utf-8"); + let modified = false; + + for (const op of item.operations || []) { + if (op.type === "replace" && op.old && op.new) { + if (fileContent.includes(op.old)) { + fileContent = fileContent.replace(op.old, op.new); + modified = true; + console.log(` โœ… Replaced content (legacy)`); + } else { + console.log(` โš ๏ธ Old string not found for replacement (legacy)`); + } + } else { + console.log( + ` โš ๏ธ Unsupported legacy operation: ${JSON.stringify(op)}`, + ); + } + } + + if (modified) { + await fs.writeFile(fullTargetPath, fileContent); + appliedCount++; + } + } + + return appliedCount; + } + + // Current object format: { "path": { action: 'replace', content: '...', oldString: '...' } } for (const [targetPath, operations] of Object.entries(patches)) { const fullTargetPath = path.join(srcDir, targetPath); console.log(` โ†’ Processing: ${targetPath}`); - // Ensure target file exists - try { - await fs.access(fullTargetPath); - } catch { + // Try both srcDir location and build root (for tsconfig located at build/) + let candidatePaths = [fullTargetPath, path.join(projectRoot, targetPath)]; + + let found = false; + let actualPath; + for (const p of candidatePaths) { + try { + await fs.access(p); + found = true; + actualPath = p; + break; + } catch (err) { + // continue + } + } + + if (!found) { console.log(` โš ๏ธ Target file not found: ${targetPath}`); continue; } - let fileContent = await fs.readFile(fullTargetPath, "utf-8"); + let fileContent = await fs.readFile(actualPath, "utf-8"); let modified = false; - // Apply operations if (operations.content && operations.action) { - const { content, action } = operations; + const { content, action, oldString } = operations; switch (action) { case "append": - // Check if content already exists to avoid duplicates if (!fileContent.includes(content.trim())) { fileContent += "\n\n" + content; modified = true; @@ -124,13 +187,9 @@ async function applyJsonPatch(patchFilePath, srcDir) { } break; case "replace": - if (operations.oldString) { - const newContent = fileContent.replace( - operations.oldString, - content, - ); - if (newContent !== fileContent) { - fileContent = newContent; + if (oldString) { + if (fileContent.includes(oldString)) { + fileContent = fileContent.replace(oldString, content); modified = true; console.log(` โœ… Replaced content`); } else { @@ -143,11 +202,12 @@ async function applyJsonPatch(patchFilePath, srcDir) { default: console.log(` โš ๏ธ Unknown action: ${action}`); } + } else { + console.log(` โš ๏ธ Invalid patch format for: ${targetPath}`); } - // Write back if modified if (modified) { - await fs.writeFile(fullTargetPath, fileContent); + await fs.writeFile(actualPath, fileContent); appliedCount++; } } diff --git a/src/overrides/fix-schemas.json b/src/overrides/fix-schemas.json index e69de29..0967ef4 100644 --- a/src/overrides/fix-schemas.json +++ b/src/overrides/fix-schemas.json @@ -0,0 +1 @@ +{} diff --git a/src/overrides/package-build.patch.json b/src/overrides/package-build.patch.json new file mode 100644 index 0000000..caa5702 --- /dev/null +++ b/src/overrides/package-build.patch.json @@ -0,0 +1,7 @@ +{ + "build/package.json": { + "action": "replace", + "content": "\"build\": \"tsc && chmod 755 dist/index.js\"", + "oldString": "\"build\": \"tsc && chmod 755 build/index.js\"" + } +} diff --git a/src/overrides/package-main.patch.json b/src/overrides/package-main.patch.json new file mode 100644 index 0000000..516825c --- /dev/null +++ b/src/overrides/package-main.patch.json @@ -0,0 +1,7 @@ +{ + "build/package.json": { + "action": "replace", + "content": "\"main\": \"dist/index.js\"", + "oldString": "\"main\": \"build/index.js\"" + } +} diff --git a/src/overrides/package-start.patch.json b/src/overrides/package-start.patch.json new file mode 100644 index 0000000..58aa791 --- /dev/null +++ b/src/overrides/package-start.patch.json @@ -0,0 +1,7 @@ +{ + "build/package.json": { + "action": "replace", + "content": "\"start\": \"node dist/index.js\"", + "oldString": "\"start\": \"node build/index.js\"" + } +} diff --git a/src/overrides/tsconfig.patch.json b/src/overrides/tsconfig.patch.json new file mode 100644 index 0000000..23ac74a --- /dev/null +++ b/src/overrides/tsconfig.patch.json @@ -0,0 +1,7 @@ +{ + "build/tsconfig.json": { + "action": "replace", + "content": "\"outDir\": \"./dist\"", + "oldString": "\"outDir\": \"./build\"" + } +} diff --git a/src/runtime/cache.js b/src/runtime/cache.js index e71b4fb..e3a024b 100644 --- a/src/runtime/cache.js +++ b/src/runtime/cache.js @@ -6,12 +6,10 @@ * Manages cached generated MCP server code for different Windmill versions. */ -import fs from 'fs/promises'; -import path from 'path'; -import os from 'os'; -import { createWriteStream } from 'fs'; -import { pipeline } from 'stream/promises'; -import * as tar from 'tar'; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import * as tar from "tar"; /** * Get cache directory for a specific Windmill version @@ -20,7 +18,8 @@ import * as tar from 'tar'; */ export function getCacheDir(windmillVersion) { const baseDir = - process.env.WINDMILL_MCP_CACHE_DIR || path.join(os.homedir(), '.cache', 'windmill-mcp'); + process.env.WINDMILL_MCP_CACHE_DIR || + path.join(os.homedir(), ".cache", "windmill-mcp"); return path.join(baseDir, windmillVersion); } @@ -31,7 +30,7 @@ export function getCacheDir(windmillVersion) { */ export async function isCached(windmillVersion) { const cacheDir = getCacheDir(windmillVersion); - const indexPath = path.join(cacheDir, 'index.js'); + const indexPath = path.join(cacheDir, "index.js"); try { await fs.access(indexPath); @@ -54,7 +53,7 @@ export async function extractToCache(tarballBuffer, windmillVersion) { await fs.mkdir(cacheDir, { recursive: true }); // Write tarball to temp file - const tempTarball = path.join(cacheDir, 'temp.tar.gz'); + const tempTarball = path.join(cacheDir, "temp.tar.gz"); await fs.writeFile(tempTarball, tarballBuffer); try { @@ -100,7 +99,7 @@ export async function saveToCache(sourceDir, windmillVersion) { * @returns {string} Path to cached code */ export function getCachedCodePath(windmillVersion) { - return path.join(getCacheDir(windmillVersion), 'index.js'); + return path.join(getCacheDir(windmillVersion), "index.js"); } /** @@ -114,7 +113,7 @@ export async function clearCache(windmillVersion) { await fs.rm(cacheDir, { recursive: true, force: true }); console.log(`๐Ÿ—‘๏ธ Cleared cache for version: ${windmillVersion}`); } else { - const baseDir = path.join(os.homedir(), '.cache', 'windmill-mcp'); + const baseDir = path.join(os.homedir(), ".cache", "windmill-mcp"); await fs.rm(baseDir, { recursive: true, force: true }); console.log(`๐Ÿ—‘๏ธ Cleared all cache`); } @@ -125,11 +124,13 @@ export async function clearCache(windmillVersion) { * @returns {Promise} List of cached version strings */ export async function listCachedVersions() { - const baseDir = path.join(os.homedir(), '.cache', 'windmill-mcp'); + const baseDir = path.join(os.homedir(), ".cache", "windmill-mcp"); try { const entries = await fs.readdir(baseDir, { withFileTypes: true }); - return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name); + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); } catch { return []; } diff --git a/src/runtime/generator.js b/src/runtime/generator.js index 172345f..0818729 100644 --- a/src/runtime/generator.js +++ b/src/runtime/generator.js @@ -6,11 +6,11 @@ * Generates MCP server code locally when artifacts are not available. */ -import { execSync } from 'child_process'; -import path from 'path'; -import fs from 'fs/promises'; -import { fileURLToPath } from 'url'; -import { saveToCache, getCacheDir } from './cache.js'; +import { execSync } from "child_process"; +import path from "path"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; +import { saveToCache } from "./cache.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -21,10 +21,12 @@ const __dirname = path.dirname(__filename); * @returns {Promise} */ export async function generateLocally(windmillVersion) { - console.log(`๐Ÿ”ง Generating MCP server locally for Windmill ${windmillVersion}...`); + console.log( + `๐Ÿ”ง Generating MCP server locally for Windmill ${windmillVersion}...`, + ); - const projectRoot = path.resolve(__dirname, '..', '..'); - const generatorDir = path.join(projectRoot, 'src', 'generator'); + const projectRoot = path.resolve(__dirname, "..", ".."); + const generatorDir = path.join(projectRoot, "src", "generator"); try { // Fetch OpenAPI spec @@ -33,9 +35,9 @@ export async function generateLocally(windmillVersion) { // Run generator console.log(`โš™๏ธ Running generator...`); - execSync('npm run generate', { + execSync("npm run generate", { cwd: projectRoot, - stdio: 'inherit', + stdio: "inherit", env: { ...process.env, WINDMILL_VERSION: windmillVersion, @@ -43,7 +45,7 @@ export async function generateLocally(windmillVersion) { }); // The generated code should be in build/ directory - const generatedDir = path.join(projectRoot, 'build'); + const generatedDir = path.join(projectRoot, "build"); // Save to cache await saveToCache(generatedDir, windmillVersion); @@ -65,8 +67,8 @@ async function fetchOpenAPISpec(windmillVersion, generatorDir) { // Determine spec URL based on version let specUrl; - if (windmillVersion === 'latest') { - specUrl = 'https://app.windmill.dev/api/openapi.json'; + if (windmillVersion === "latest") { + specUrl = "https://app.windmill.dev/api/openapi.json"; } else { // For specific versions, try to get from Windmill's GitHub releases // This is a fallback approach - ideally we'd have a way to get specific version specs @@ -76,10 +78,10 @@ async function fetchOpenAPISpec(windmillVersion, generatorDir) { console.log(` Spec URL: ${specUrl}`); // Run fetch-spec with custom URL - const { execSync } = await import('child_process'); - execSync('npm run fetch-spec', { + const { execSync } = await import("child_process"); + execSync("npm run fetch-spec", { cwd: path.dirname(generatorDir), - stdio: 'inherit', + stdio: "inherit", env: { ...process.env, OPENAPI_SPEC_URL: specUrl, @@ -92,8 +94,13 @@ async function fetchOpenAPISpec(windmillVersion, generatorDir) { * @returns {Promise} Whether generator is available */ export async function isGeneratorAvailable() { - const projectRoot = path.resolve(__dirname, '..', '..'); - const generatorPath = path.join(projectRoot, 'src', 'generator', 'generate.js'); + const projectRoot = path.resolve(__dirname, "..", ".."); + const generatorPath = path.join( + projectRoot, + "src", + "generator", + "generate.js", + ); try { await fs.access(generatorPath); diff --git a/src/runtime/index.js b/src/runtime/index.js index 1ccce77..9c94b4c 100644 --- a/src/runtime/index.js +++ b/src/runtime/index.js @@ -10,16 +10,17 @@ * 4. Loads and runs the MCP server */ -import { fileURLToPath } from 'url'; -import path from 'path'; -import { isCached, getCachedCodePath, listCachedVersions, clearCache } from './cache.js'; -import { downloadArtifact, listAvailableVersions } from './downloader.js'; -import { generateLocally, isGeneratorAvailable } from './generator.js'; -import { createRequire } from 'module'; +import { + isCached, + getCachedCodePath, + listCachedVersions, + clearCache, +} from "./cache.js"; +import { downloadArtifact, listAvailableVersions } from "./downloader.js"; +import { generateLocally, isGeneratorAvailable } from "./generator.js"; +import { createRequire } from "module"; const require = createRequire(import.meta.url); -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); /** * Main entry point @@ -29,40 +30,40 @@ async function main() { const args = process.argv.slice(2); // Handle CLI commands - if (args.includes('--clear-cache')) { - const version = args[args.indexOf('--clear-cache') + 1]; + if (args.includes("--clear-cache")) { + const version = args[args.indexOf("--clear-cache") + 1]; await clearCache(version); return; } - if (args.includes('--list-cached')) { + if (args.includes("--list-cached")) { const versions = await listCachedVersions(); - console.log('๐Ÿ“ฆ Cached versions:'); - versions.forEach(v => console.log(` - ${v}`)); + console.log("๐Ÿ“ฆ Cached versions:"); + versions.forEach((v) => console.log(` - ${v}`)); return; } - if (args.includes('--list-available')) { + if (args.includes("--list-available")) { const versions = await listAvailableVersions(); - console.log('๐ŸŒ Available versions:'); - versions.forEach(v => console.log(` - ${v}`)); + console.log("๐ŸŒ Available versions:"); + versions.forEach((v) => console.log(` - ${v}`)); return; } - if (args.includes('--help') || args.includes('-h')) { + if (args.includes("--help") || args.includes("-h")) { printHelp(); return; } // Get Windmill version from environment variable - const windmillVersion = process.env.WINDMILL_VERSION || 'latest'; - const packageJson = require('../../package.json'); + const windmillVersion = process.env.WINDMILL_VERSION || "latest"; + const packageJson = require("../../package.json"); const packageVersion = packageJson.version; - console.log('๐Ÿš€ Starting Windmill MCP Server'); + console.log("๐Ÿš€ Starting Windmill MCP Server"); console.log(` Windmill version: ${windmillVersion}`); console.log(` Package version: ${packageVersion}`); - console.log(''); + console.log(""); // Check if cached if (await isCached(windmillVersion)) { @@ -86,7 +87,9 @@ async function main() { if (!(await isGeneratorAvailable())) { console.error(`โŒ Generator not available.`); - console.error(` Please install the full package or use a released version.`); + console.error( + ` Please install the full package or use a released version.`, + ); process.exit(1); } @@ -101,16 +104,18 @@ async function main() { async function loadMCPServer(windmillVersion) { const mcpServerPath = getCachedCodePath(windmillVersion); - console.log(''); + console.log(""); console.log(`๐ŸŽฏ Loading MCP server from: ${mcpServerPath}`); - console.log(''); + console.log(""); try { // Dynamically import the generated MCP server await import(mcpServerPath); } catch (error) { console.error(`โŒ Failed to load MCP server: ${error.message}`); - console.error(` Cache may be corrupted. Try: --clear-cache ${windmillVersion}`); + console.error( + ` Cache may be corrupted. Try: --clear-cache ${windmillVersion}`, + ); process.exit(1); } } @@ -154,6 +159,6 @@ Examples: // Run main function main().catch((error) => { - console.error('Fatal error:', error); + console.error("Fatal error:", error); process.exit(1); }); diff --git a/src/utils/tool-categories.js b/src/utils/tool-categories.js new file mode 100644 index 0000000..a438ff6 --- /dev/null +++ b/src/utils/tool-categories.js @@ -0,0 +1,164 @@ +// Centralized tool category patterns and helpers + +export const patterns = { + // Job Management + "job:list": + /^.*(?:getJob|listJobs|listCompletedJobs|listExtendedJobs|countJobs|countCompletedJobs|searchJobs|getCompletedJob|getCompletedCount|getCompletedJobResult|getCompletedJobResultMaybe|listFilteredJobsUuids|countJobsByTag).*$/i, + "job:run": + /^.*(?:runScript|runFlow|runWaitResult|runSlackMessageTest|runTeamsMessageTest).*$/i, + "job:manage": + /^.*(?:cancel|resume|forceCancel|batchReRun|setJobProgress|deleteCompletedJob|cancelPersistentQueuedJobs|getRootJobId|getConcurrencyKey|createJobSignature|getSuspendedJobFlow).*$/i, + + // User Management + "user:auth": + /^.*(?:login|logout|token|refreshToken|setPassword|blacklist).*$/i, + "user:profile": + /^.*(?:getUser|updateUser|whoami|whois|usernameToEmail|getCurrentEmail).*$/i, + "user:admin": + /^.*(?:addUser|createAccount|deleteUser|createUserGlobally|globalUser|setLoginTypeForUser|setPasswordForUser|existsEmail|existsUsername).*$/i, + "user:groups": + /^.*(?:addUserToGroup|removeUserToGroup|addUserToInstanceGroup|removeUserFromInstanceGroup|createInstanceGroup|deleteInstanceGroup|getInstanceGroup|listInstanceGroups|exportInstanceGroups).*$/i, + "user:invites": + /^.*(?:invite|acceptInvite|declineInvite|listPendingInvites).*$/i, + + // Workspace Management + "workspace:list": + /^.*(?:listWorkspaces|listUserWorkspaces|getWorkspace|existsWorkspace).*$/i, + "workspace:manage": + /^.*(?:createWorkspace|deleteWorkspace|archiveWorkspace|unarchiveWorkspace|changeWorkspace|leaveWorkspace|deleteFromWorkspace|installFromWorkspace).*$/i, + "workspace:settings": + /^.*(?:editWorkspace|setWorkspaceEncryptionKey|getWorkspaceEncryptionKey).*$/i, + "workspace:usage": /^.*(?:getWorkspaceUsage|workspaceAcknowledge).*$/i, + + // Script Management + "script:list": + /^.*(?:listScripts|getScript|listScriptPaths|existsScript|getRunnable|isOwnerOfPath|customPathExists|existsRoute).*$/i, + "script:manage": + /^.*(?:createScript|updateScript|deleteScript|archiveScript|executeComponent).*$/i, + "script:run": + /^.*(?:runScript|runRawScript|runScriptPreview|runWaitResultScript).*$/i, + "script:hub": /^.*(?:getHubScript|queryHubScripts|getTopHubScripts).*$/i, + + // Flow Management + "flow:list": /^.*(?:listFlows|getFlow|listFlowPaths|existsFlow).*$/i, + "flow:manage": /^.*(?:createFlow|updateFlow|deleteFlow|archiveFlow).*$/i, + "flow:run": /^.*(?:runFlow|runFlowPreview|runWaitResultFlow|restartFlow).*$/i, + "flow:hub": /^.*(?:getHubFlow|listHubFlows).*$/i, + + // Resource Management + "resource:list": + /^.*(?:listResource|getResource|existsResource|queryResourceTypes).*$/i, + "resource:manage": /^.*(?:createResource|updateResource|deleteResource).*$/i, + "resource:types": /^.*(?:ResourceType|fileResourceTypeToFileExtMap).*$/i, + + // Variable Management + "variable:list": + /^.*(?:listVariable|getVariable|existsVariable|listContextualVariables|encryptValue).*$/i, + "variable:manage": + /^.*(?:createVariable|updateVariable|deleteVariable|setEnvironmentVariable).*$/i, + + // Schedule Management + "schedule:list": /^.*(?:listSchedules|getSchedule|existsSchedule).*$/i, + "schedule:manage": + /^.*(?:createSchedule|updateSchedule|deleteSchedule|setScheduleEnabled|previewSchedule).*$/i, + + // Trigger Management + "trigger:list": /^.*(?:list.*Trigger|get.*Trigger|exists.*Trigger).*$/i, + "trigger:manage": + /^.*(?:create.*Trigger|update.*Trigger|delete.*Trigger|set.*TriggerEnabled).*$/i, + "trigger:test": /^.*(?:test.*Connection|test.*Trigger).*$/i, + + // App Management + "app:list": + /^.*(?:listApps|getApp|existsApp|listAppPaths|existsRawApp|getRawAppData|listRawApps|getPublicAppByCustomPath|getPublicAppBySecret|getPublicSecretOfApp).*$/i, + "app:manage": + /^.*(?:createApp|createRawApp|updateApp|deleteApp|deleteRawApp).*$/i, + "app:deploy": /^.*(?:getAppHistory|getAppVersion|updateAppHistory).*$/i, + "app:hub": /^.*(?:getHubApp|listHubApps).*$/i, + + // Folder Management + "folder:list": + /^.*(?:listFolders|getFolder|existsFolder|listFolderNames).*$/i, + "folder:manage": /^.*(?:createFolder|updateFolder|deleteFolder).*$/i, + "folder:permissions": + /^.*(?:addOwnerToFolder|removeOwnerToFolder|getGranularAcls|addGranularAcls|removeGranularAcls).*$/i, + + // Group Management + "group:list": /^.*(?:listGroups|getGroup|listGroupNames).*$/i, + "group:manage": /^.*(?:createGroup|updateGroup|deleteGroup).*$/i, + "group:concurrency": + /^.*(?:listConcurrencyGroups|deleteConcurrencyGroup).*$/i, + + // Database & Storage + "storage:list": + /^.*(?:listStoredFiles|loadFile|listAssets|listLogFiles|getLogFile|getLogFileFromStore|fileDownload|fileDownloadParquetAsCsv).*$/i, + "storage:manage": + /^.*(?:fileUpload|deleteS3File|moveS3File|get public resource).*$/i, + "storage:db": + /^.*(?:databasesExist|duckdbConnection|postgres|getDbClock).*$/i, + + // Integration & OAuth + "integration:oauth": + /^.*(?:OAuth|connect|disconnect|loginWithOauth|polarsConnectionSettings|getOAuthConnect|listOAuthConnects|getGlobalConnectedRepositories).*$/i, + "integration:slack": /^.*(?:slack|Slack).*$/i, + "integration:teams": /^.*(?:teams|Teams).*$/i, + "integration:github": + /^.*(?:github|Github|editGitSyncRepository|deleteGitSyncRepository).*$/i, + "integration:gcp": + /^.*(?:deleteGcpSubscription|listAllTGoogleTopicSubscriptions|listGoogleTopics).*$/i, + "integration:hub": /^.*(?:listHubIntegrations).*$/i, + + // Audit & Logging + "audit:list": /^.*(?:getAuditLog|listAuditLogs).*$/i, + "audit:search": + /^.*(?:searchLogs|countSearchLogs|clearIndex|ListAvailableScopes).*$/i, + + // Settings & Configuration + "settings:general": + /^.*(?:getSettings|updateConfig|listConfigs|get config|deleteConfig|editDefaultScripts|get default scripts|listGlobalSettings|getGlobal|getLocal|editCopilotConfig|getCopilotInfo|editWebhook|editErrorHandler|editDeployTo|getDeployTo).*$/i, + "settings:system": + /^.*(?:backendVersion|getLicenseId|sendStats|getUsage|geDefaultTags|getCustomTags|isDefaultTagsPerWorkspace).*$/i, + "settings:storage": + /^.*(?:editLargeFileStorage|testObjectStorage|getLargeFileStorageConfig|getSecondaryStorageNames|editDucklakeConfig|createDucklakeDatabase|listDucklakes).*$/i, + + // System & Health + "system:health": /^.*(?:backendUptodate|pingCaptureConfig).*$/i, + "system:usage": /^.*(?:getUsage|listAutoscalingEvents).*$/i, + "system:critical": + /^.*(?:acknowledgeCriticalAlert|acknowledgeAllCriticalAlerts|getCriticalAlerts|getThresholdAlert).*$/i, + "system:deployment": /^.*(?:deployment|Deployment).*$/i, + "system:workers": + /^.*(?:listWorkers|listWorkerGroups|existsWorkerWithTag|getCountsOfJobsWaitingPerTag|listQueue|getQueueCount|getQueueMetrics|listFilteredQueueUuids).*$/i, + "system:instance": + /^.*(?:leaveInstance|exportInstallation|importInstallation|isDomainAllowed|getIsPremium|getPremiumInfo|createCustomerPortalSession).*$/i, + + // Captures & Inputs + "capture:manage": /^.*(?:capture|Capture).*$/i, + "input:manage": /^.*(?:input|Input).*$/i, + + // Drafts & Templates + "draft:manage": /^.*(?:draft|Draft).*$/i, + "template:manage": /^.*(?:template|Template).*$/i, + + // Tutorials & Onboarding + "tutorial:manage": /^.*(?:tutorial|Tutorial).*$/i, + + // OpenAPI & MCP + "system:api": + /^.*(?:DownloadOpenapiSpec|generateOpenapiSpec|getOpenApiYaml|listMcpTools|listAvailablePythonVersions).*$/i, + + // Miscellaneous - catch remaining + "misc:general": /.*/, // Final catch-all +}; + +export function categorizeTool(operationId) { + for (const [category, regex] of Object.entries(patterns)) { + if (regex.test(operationId)) return category; + } + return "misc:general"; +} + +export function getNamespace(operationId) { + // Returns the category with ':' replaced by '_' to match older code expectations + return categorizeTool(operationId).replace(":", "_"); +} diff --git a/tests/config.json b/tests/config.json index d78b3cb..3deab50 100644 --- a/tests/config.json +++ b/tests/config.json @@ -1,13 +1,14 @@ { "windmill": { - "baseUrl": "https://app.windmill.dev", - "workspace": "demo", - "timeout": 30000 + "baseUrl": "http://localhost:8000", + "workspace": "admins", + "timeout": 30000, + "superadminSecret": "test-super-secret" }, "test": { "integration": { "enabled": true, - "skipOnMissingCredentials": true + "skipOnMissingCredentials": false }, "unit": { "enabled": true diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index 38db1ea..41c947e 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -3,39 +3,43 @@ # This is a simplified single-container setup for testing purposes. # For production, use the full multi-container setup from Windmill docs. -version: '3.8' +# Minimal Windmill Docker Setup for E2E Testing + +# Define all available tags for maximum worker coverage +x-windmill-all-tags: &windmill-all-tags deno,python3,bash,go,powershell,dependency,flow,other,bun,php,rust,ansible,nativets,postgresql,mysql,graphql,snowflake,mssql,bigquery,csharp,java,ruby + +# Define common configuration using YAML anchors +x-windmill-common-env: &windmill-common-env + DATABASE_URL: postgresql://postgres:changeme@windmill-db:5432/windmill?sslmode=disable + SUPERADMIN_SECRET: test-super-secret + RUST_LOG: info + BASE_URL: http://localhost:8000 + # Set all possible worker tags + WORKER_TAGS: *windmill-all-tags + +x-windmill-image: &windmill-image ghcr.io/windmill-labs/windmill:main services: - # Windmill server with embedded database and worker - windmill: - image: ghcr.io/windmill-labs/windmill:main - container_name: windmill-test + # Windmill server (only server mode) + windmill-server: + image: *windmill-image + container_name: windmill-server-test ports: - "8000:8000" environment: - # Database (embedded SQLite for simplicity) - - DATABASE_URL=postgresql://postgres:changeme@windmill-db:5432/windmill?sslmode=disable - - # Server configuration - - MODE=server - - NUM_WORKERS=1 - - DISABLE_NSJAIL=true - - WORKER_TAGS=deno,python3,bash - - # Authentication (test mode) - - SUPERADMIN_SECRET=test-super-secret - - # Other settings - - RUST_LOG=info - - BASE_URL=http://localhost:8000 - + # Inherit common environment variables + <<: *windmill-common-env + MODE: server # Explicitly set mode to server only + NUM_WORKERS: 0 # Disable embedded workers + DISABLE_NSJAIL: true + volumes: - windmill_data:/tmp/windmill - + depends_on: windmill-db: condition: service_healthy - + healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/version"] interval: 10s @@ -43,23 +47,33 @@ services: retries: 5 start_period: 30s + # Dedicated Windmill worker service, configured for all tags + windmill-worker: + image: *windmill-image + container_name: windmill-worker-test + restart: unless-stopped + environment: + # Inherit common environment variables, including all tags + <<: *windmill-common-env + MODE: worker # Explicitly set mode to worker + # PostgreSQL database windmill-db: image: postgres:14 container_name: windmill-db-test environment: - - POSTGRES_PASSWORD=changeme - - POSTGRES_DB=windmill - + POSTGRES_PASSWORD: changeme + POSTGRES_DB: windmill + volumes: - windmill_db_data:/var/lib/postgresql/data - + healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 - + ports: - "5432:5432" diff --git a/tests/docker/wait-for-windmill.js b/tests/docker/wait-for-windmill.js index fea26a6..b523b73 100644 --- a/tests/docker/wait-for-windmill.js +++ b/tests/docker/wait-for-windmill.js @@ -2,14 +2,14 @@ /** * Wait for Windmill to be ready - * + * * This script waits for a Windmill instance to be healthy and ready to accept requests. * Useful for CI/CD and automated testing. */ -import http from 'http'; +import http from "http"; -const WINDMILL_URL = process.env.E2E_WINDMILL_URL || 'http://localhost:8000'; +const WINDMILL_URL = process.env.E2E_WINDMILL_URL || "http://localhost:8000"; const MAX_ATTEMPTS = 60; // 5 minutes with 5 second intervals const RETRY_INTERVAL = 5000; // 5 seconds @@ -18,8 +18,8 @@ const RETRY_INTERVAL = 5000; // 5 seconds */ async function checkWindmill() { return new Promise((resolve) => { - const url = new URL('/api/version', WINDMILL_URL); - + const url = new URL("/api/version", WINDMILL_URL); + const req = http.get(url, (res) => { if (res.statusCode === 200) { resolve(true); @@ -27,11 +27,11 @@ async function checkWindmill() { resolve(false); } }); - - req.on('error', () => { + + req.on("error", () => { resolve(false); }); - + req.setTimeout(5000, () => { req.destroy(); resolve(false); @@ -44,32 +44,34 @@ async function checkWindmill() { */ async function waitForWindmill() { console.log(`โณ Waiting for Windmill at ${WINDMILL_URL}...`); - + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { const ready = await checkWindmill(); - + if (ready) { console.log(`โœ… Windmill is ready! (attempt ${attempt}/${MAX_ATTEMPTS})`); return true; } - + if (attempt < MAX_ATTEMPTS) { - process.stdout.write(` Attempt ${attempt}/${MAX_ATTEMPTS} - not ready yet, retrying in ${RETRY_INTERVAL/1000}s...\r`); - await new Promise(resolve => setTimeout(resolve, RETRY_INTERVAL)); + process.stdout.write( + ` Attempt ${attempt}/${MAX_ATTEMPTS} - not ready yet, retrying in ${RETRY_INTERVAL / 1000}s...\r`, + ); + await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL)); } } - - console.error(`\nโŒ Windmill did not become ready after ${MAX_ATTEMPTS} attempts`); + + console.error( + `\nโŒ Windmill did not become ready after ${MAX_ATTEMPTS} attempts`, + ); return false; } // Run if executed directly // In ES modules, we check if this is the main module using import.meta.url -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); // Check if this script is being run directly if (process.argv[1] === __filename) { @@ -78,7 +80,7 @@ if (process.argv[1] === __filename) { process.exit(ready ? 0 : 1); }) .catch((error) => { - console.error('Error:', error); + console.error("Error:", error); process.exit(1); }); } diff --git a/tests/e2e/mcp-integration.test.js b/tests/e2e/mcp-integration.test.js index 055cf5f..5cf4c1b 100644 --- a/tests/e2e/mcp-integration.test.js +++ b/tests/e2e/mcp-integration.test.js @@ -27,13 +27,13 @@ const __dirname = path.dirname(__filename); const isE2EEnabled = (process.env.E2E_WINDMILL_URL || process.env.WINDMILL_BASE_URL) && (process.env.E2E_WINDMILL_TOKEN || process.env.WINDMILL_API_TOKEN); -const workspace = process.env.E2E_WORKSPACE || "demo"; +let workspace; /** * Create an MCP client connected to our MCP server */ async function createMCPClient() { - const serverPath = path.join(__dirname, "../../build/build/index.js"); + const serverPath = path.join(__dirname, "../../build/dist/index.js"); // Create transport const transport = new StdioClientTransport({ @@ -45,6 +45,11 @@ async function createMCPClient() { process.env.E2E_WINDMILL_URL || process.env.WINDMILL_BASE_URL, WINDMILL_API_TOKEN: process.env.E2E_WINDMILL_TOKEN || process.env.WINDMILL_API_TOKEN, + // Map common token env vars to generated-server expected scheme-specific vars + BEARER_TOKEN_BEARERAUTH: + process.env.E2E_WINDMILL_TOKEN || process.env.WINDMILL_API_TOKEN, + API_KEY_COOKIEAUTH: + process.env.E2E_WINDMILL_TOKEN || process.env.WINDMILL_API_TOKEN, }, }); @@ -62,6 +67,28 @@ async function createMCPClient() { // Connect await client.connect(transport); + // Backwards-compatibility wrapper for client.request: + // The SDK now expects (request, resultSchema, options). Older tests + // passed options as the second argument. Wrap client.request so tests + // can continue to call client.request(req, options) or client.request(req). + const origRequest = client.request.bind(client); + client.request = (req, maybeSchemaOrOptions, maybeOptions) => { + // If the second arg looks like options (no .parse), treat it as options. + if ( + maybeSchemaOrOptions && + typeof maybeSchemaOrOptions === "object" && + typeof maybeSchemaOrOptions.parse !== "function" + ) { + return origRequest(req, { parse: (v) => v }, maybeSchemaOrOptions); + } + // If no schema provided, default to identity parser and pass through options + if (maybeSchemaOrOptions === undefined) { + return origRequest(req, { parse: (v) => v }, maybeOptions); + } + // Otherwise assume caller provided a proper schema + return origRequest(req, maybeSchemaOrOptions, maybeOptions); + }; + return { client, transport }; } @@ -93,6 +120,32 @@ describe.skipIf(!isE2EEnabled)("MCP Server E2E Tests", () => { { timeout: 10000 }, ); expect(pingResponse).toHaveProperty("tools"); + + // Determine a workspace to use for workspace-scoped tests. + // Prefer the explicit env var, otherwise query Windmill for available workspaces. + if (process.env.E2E_WORKSPACE) { + workspace = process.env.E2E_WORKSPACE; + } else { + try { + const workspacesResp = await mcpClient.request( + { + method: "tools/call", + params: { name: "listWorkspaces", arguments: {} }, + }, + { timeout: 10000 }, + ); + if (!workspacesResp.isError) { + const workspaces = JSON.parse(workspacesResp.content[0].text); + if (Array.isArray(workspaces) && workspaces.length > 0) { + // Prefer id, fall back to name + workspace = workspaces[0].id || workspaces[0].name; + } + } + } catch (err) { + // If workspace discovery fails, keep workspace undefined so tests that require it can fail explicitly. + console.warn("Workspace discovery failed:", err?.message || err); + } + } } catch (error) { console.error("Failed to initialize MCP server:", error.message); throw new Error( diff --git a/tests/setup.js b/tests/setup.js index 0970eb2..c6ab888 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -20,11 +20,17 @@ beforeAll(() => { process.env.NODE_ENV = "test"; // Only set defaults if environment variables are not already set + // These defaults match the Docker dev setup (npm run docker:dev) if (!process.env.WINDMILL_BASE_URL && !process.env.E2E_WINDMILL_URL) { process.env.WINDMILL_BASE_URL = "http://localhost:8000"; } if (!process.env.WINDMILL_API_TOKEN && !process.env.E2E_WINDMILL_TOKEN) { - process.env.WINDMILL_API_TOKEN = "test-token"; + // Use superadmin secret from Docker setup for local testing + process.env.WINDMILL_API_TOKEN = "test-super-secret"; + } + if (!process.env.E2E_WORKSPACE) { + // Default workspace created by Windmill + process.env.E2E_WORKSPACE = "admins"; } }); diff --git a/tests/unit/mocks.test.js b/tests/unit/mocks.test.js index c2d19c0..fad8e0d 100644 --- a/tests/unit/mocks.test.js +++ b/tests/unit/mocks.test.js @@ -1,104 +1,102 @@ /** * Unit tests for Mock Windmill Client - * + * * These tests demonstrate testing MCP tools without connecting to a real Windmill instance. */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach } from "vitest"; import { MockWindmillClient, - mockJob, - mockScript, createMockToolRequest, createMockToolResponse, -} from '../utils/mocks.js'; +} from "../utils/mocks.js"; -describe('MockWindmillClient', () => { +describe("MockWindmillClient", () => { let client; beforeEach(() => { client = new MockWindmillClient(); }); - describe('Job Operations', () => { - it('should list jobs', async () => { - const jobs = await client.listJobs('test-workspace'); - + describe("Job Operations", () => { + it("should list jobs", async () => { + const jobs = await client.listJobs("test-workspace"); + expect(jobs).toBeInstanceOf(Array); expect(jobs).toHaveLength(1); expect(jobs[0]).toMatchObject({ id: expect.any(String), - workspace: 'test-workspace', - status: 'completed', + workspace: "test-workspace", + status: "completed", }); }); - it('should get a specific job', async () => { - const job = await client.getJob('test-workspace', 'job-123'); - + it("should get a specific job", async () => { + const job = await client.getJob("test-workspace", "job-123"); + expect(job).toMatchObject({ - id: 'job-123', - workspace: 'test-workspace', - status: 'completed', + id: "job-123", + workspace: "test-workspace", + status: "completed", }); }); - it('should run a script', async () => { - const job = await client.runScript('test-workspace', 'f/test/script', { - arg1: 'value1', + it("should run a script", async () => { + const job = await client.runScript("test-workspace", "f/test/script", { + arg1: "value1", }); - + expect(job).toMatchObject({ id: expect.any(String), - workspace: 'test-workspace', + workspace: "test-workspace", }); }); - it('should record API calls', async () => { - await client.listJobs('test-workspace'); - await client.getJob('test-workspace', 'job-123'); - + it("should record API calls", async () => { + await client.listJobs("test-workspace"); + await client.getJob("test-workspace", "job-123"); + const history = client.getCallHistory(); expect(history).toHaveLength(2); expect(history[0]).toMatchObject({ - method: 'GET', - path: '/api/w/test-workspace/jobs/list', + method: "GET", + path: "/api/w/test-workspace/jobs/list", }); expect(history[1]).toMatchObject({ - method: 'GET', - path: '/api/w/test-workspace/jobs/get/job-123', + method: "GET", + path: "/api/w/test-workspace/jobs/get/job-123", }); }); }); - describe('Script Operations', () => { - it('should list scripts', async () => { - const scripts = await client.listScripts('test-workspace'); - + describe("Script Operations", () => { + it("should list scripts", async () => { + const scripts = await client.listScripts("test-workspace"); + expect(scripts).toBeInstanceOf(Array); expect(scripts).toHaveLength(1); expect(scripts[0]).toMatchObject({ hash: expect.any(String), path: expect.any(String), - language: 'python3', + language: "python3", }); }); - it('should get a specific script', async () => { - const script = await client.getScript('test-workspace', 'f/test/script'); - + it("should get a specific script", async () => { + const script = await client.getScript("test-workspace", "f/test/script"); + expect(script).toMatchObject({ - hash: 'script-hash-abc', - path: 'f/test/script', - language: 'python3', + hash: "script-hash-abc", + path: "f/test/script", + language: "python3", }); }); }); - describe('Workflow Operations', () => { - it('should list workflows', async () => { - const workflows = await client.listWorkflows('test-workspace'); - + describe("Workflow Operations", () => { + it("should list workflows", async () => { + const workflows = await client.listWorkflows("test-workspace"); + expect(workflows).toBeInstanceOf(Array); expect(workflows).toHaveLength(1); expect(workflows[0]).toMatchObject({ @@ -107,60 +105,63 @@ describe('MockWindmillClient', () => { }); }); - it('should get a specific workflow', async () => { - const workflow = await client.getWorkflow('test-workspace', 'f/test/workflow'); - + it("should get a specific workflow", async () => { + const workflow = await client.getWorkflow( + "test-workspace", + "f/test/workflow", + ); + expect(workflow).toMatchObject({ - path: 'f/test/workflow', - summary: 'Test workflow', + path: "f/test/workflow", + summary: "Test workflow", value: expect.any(Object), }); }); }); - describe('Call History', () => { - it('should track all API calls', async () => { - await client.listJobs('test-workspace'); - await client.listScripts('test-workspace'); - await client.listWorkflows('test-workspace'); - + describe("Call History", () => { + it("should track all API calls", async () => { + await client.listJobs("test-workspace"); + await client.listScripts("test-workspace"); + await client.listWorkflows("test-workspace"); + expect(client.getCallHistory()).toHaveLength(3); }); - it('should clear call history', async () => { - await client.listJobs('test-workspace'); + it("should clear call history", async () => { + await client.listJobs("test-workspace"); expect(client.getCallHistory()).toHaveLength(1); - + client.clearHistory(); expect(client.getCallHistory()).toHaveLength(0); }); }); }); -describe('MCP Tool Helpers', () => { - it('should create mock tool request', () => { - const request = createMockToolRequest('list_jobs', { - workspace: 'test-workspace', +describe("MCP Tool Helpers", () => { + it("should create mock tool request", () => { + const request = createMockToolRequest("list_jobs", { + workspace: "test-workspace", }); - + expect(request).toMatchObject({ - method: 'tools/call', + method: "tools/call", params: { - name: 'list_jobs', + name: "list_jobs", arguments: { - workspace: 'test-workspace', + workspace: "test-workspace", }, }, }); }); - it('should create mock tool response', () => { + it("should create mock tool response", () => { const response = createMockToolResponse({ success: true }); - + expect(response).toMatchObject({ content: [ { - type: 'text', + type: "text", text: expect.any(String), }, ], @@ -168,14 +169,14 @@ describe('MCP Tool Helpers', () => { }); }); - it('should create error response', () => { - const response = createMockToolResponse('Error occurred', true); - + it("should create error response", () => { + const response = createMockToolResponse("Error occurred", true); + expect(response).toMatchObject({ content: [ { - type: 'text', - text: 'Error occurred', + type: "text", + text: "Error occurred", }, ], isError: true,