Compare commits

...
Author SHA1 Message Date
windmill-internal-app[bot] b195ef8b45 docs(changelog): add new entries from changelog 2025-08-14 11:02:06 +00:00
eb18d0d1f7 nit(aichat): Remove inline type restriction for ts (#6100)
* Remove inline type restriction from TypeScript AI script generation

Remove the requirement to inline object types in TypeScript AI chat prompts. The AI will now only receive guidance about using RT.ResourceType for resource types, without being forced to inline other parameter types.

Fixes #6099

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* Rename TS_INLINE_TYPE_INSTRUCTION to TS_RESOURCE_TYPE_INSTRUCTION

Co-authored-by: centdix <centdix@users.noreply.github.com>

* remove

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
2025-08-14 11:53:17 +02:00
f1492036a7 fix(aichat): better placeholders based on mode (#6378)
* feat(aichat): add mode-specific placeholders for better user guidance

- Replace generic "Ask anything" placeholder with mode-specific suggestions
- Script mode: "Modify this script, fix errors, or generate new code..."
- Flow mode: "Edit this flow, add steps, or modify workflow logic..."
- Navigator mode: "Help me navigate Windmill or find features..."
- API mode: "Make API calls to fetch data or manage resources..."
- Ask mode: "Ask questions about Windmill features and documentation..."
- Maintains backward compatibility for custom placeholders
- Uses Svelte 5 reactive $derived.by for dynamic updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* feat(aichat): make placeholder prop optional and simplify logic

- Make placeholder prop optional instead of defaulting to 'Ask anything'
- Remove !== 'ask anything' logic check, now simply checks if placeholder is provided
- When placeholder is provided, use it directly; otherwise fall back to mode-specific placeholders
- Maintains backward compatibility while simplifying the conditional logic

Co-authored-by: centdix <centdix@users.noreply.github.com>

* feat(aichat): use AIMode enum instead of hardcoded strings

- Import AIMode enum in AIChatInput.svelte
- Replace hardcoded mode strings with enum values in switch statement
- Update focusInput function and template condition to use enum
- Maintains consistency with type-safe enum usage throughout codebase

Co-authored-by: centdix <centdix@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
2025-08-14 09:23:11 +00:00
4398013e81 fix(flowEditor): add diff mode action buttons to inline script editor (#6379)
* feat(flowEditor): add diff mode action buttons to inline script editor

- Add showHistoryDrawer state to FlowModuleComponent
- Bind showHistoryDrawer to EditorBar for history functionality
- Add showButtons={diffMode} prop to DiffEditor in flow editor
- Add on:hideDiffMode and on:seeHistory event handlers to DiffEditor
- Ensures inline script editor in flows has same diff mode buttons as main script editor

Fixes #6377

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* fix

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
2025-08-14 09:22:50 +00:00
80d12426f6 fix: better gcp pubsub error status code (#6385)
* ee repo ref

* revert accidental ee-repo-ref commit to main

* revert accidental ee-repo-ref commit to main

* fix

* ref

---------

Co-authored-by: Alexander Petric <alex@windmill.dev>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-14 09:13:14 +00:00
Alexander Petric 58975b58dc feat: instance groups workspace (#6380)
* feat: instancd groups mapping to workspace

* npm run check

* nits

* improve apis

* sqlx prepare and ee-repo ref

* adding workspace assignment in groups page

* nits

* correct rollback

* sqlx

* ee repo ref

* revert accidental ee-repo-ref commit to main

* ee repo ref

* revert accidental ee-repo-ref commit to main
2025-08-14 09:06:00 +00:00
Ruben Fiszel 57155dd318 nit 2025-08-14 08:35:15 +00:00
Ruben Fiszel 24604c35ba nit flow job log improvement 2025-08-14 07:52:12 +00:00
Ruben Fiszel b82e6516ef nit flow job log improvement 2025-08-14 07:50:55 +00:00
Ruben Fiszelandrubenfiszel f250d775ce chore(main): release 1.525.0 (#6371)
* chore(main): release 1.525.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-14 00:54:42 +00:00
Ruben Fiszel 87b76c0cb6 fix sqlx 2025-08-14 00:50:14 +00:00
GuilhemandRuben Fiszel 4ec1dce531 feat(frontend): add flow log view (#6330)
* componentize detail module

* Add logs tab

* add flow log viewer

* fetch logs

* handle subflows

* add forloop iteration picker

* polish

* expand steps log by default

* move logic handling to wrapper component

* improve iteration picker

* clean code

* polishing

* Add flow start and flow end rows

* fix infinite loop

* nit

* use list instead of table

* use custom id for collapsing subflow

* remove debug logs

* Use status dot instead of text

* fetch log from moduleState

* wip

* only fetch subflow jobs from cache if job is completed

* Add job polling for expanded steps

* handle subflows

* Init logs for steps

* update localModuleState logs

* use selected iteration from local module state

* handle branchone

* Add branch one and branch all label

* remove redondant innerModule prop

* Improve UX

* Add expand/collapse

* Add filter to hide result and inputs

* Steps are now flow children

* improve UX

* Open flow and steps sction when executing

* Handle empty subflows

* remove unnecessary sequence viewer component

* nit

* use iteration picker in log view

* Replace dot with step type icon

* indicate subflows

* add step number and progression

* Incorporate inputs and results in the list of steps

* Add error indicator when subflow has error

* improve topbar

* improve log polling

* Improve log polling

* Add root flow log fetching and polling

* Add debounce for loading subflow jobs

* write a function to build the tree view from the graph

* remove unnecessary log polling

* fix flow result display

* flag errors

* preprocessor

* remove all flow logs drawer

* grenerate graph from component

* wip

* Check module change before building graph

* nit

* fix log overflow

* fix log viewer borders

* mini jobs run preview fix

* elegent job logs loading

* nit

* nit

* nit

* all

* all

* all

* all

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-14 00:39:21 +00:00
hugocasa 2b37281084 fix(frontend): do not open popup when clicking on wand in flow inline script editor (#6374)
* fix(frontend): do not open popup when clicking on wand in flow inline script editor

* nit
2025-08-13 23:17:04 +00:00
centdix 76569abb1e chore(mcp): restructure mcp related files and add annotations to tools (#6373)
* restructure mcp related files

* add annotations
2025-08-13 23:16:49 +00:00
centdix 8fb082e5f3 internal: fix exit error 3 on git action (#6383)
* fix exit error 3

* fix
2025-08-13 23:16:07 +00:00
centdix 34773f2614 feat(aichat): add test tool to script and flow mode (#6367)
* add test script tool

* modify system prompt

* cleaning

* same for flows

* cleaning

* apply code when confirm test + fix circular dep

* cleaning

* factorize

* display error

* cleaning

* fix

* update comment

* prompts

* cleaner code

* show logs in separate container

* format
2025-08-13 21:58:28 +02:00
Guilhem a41edd236b fix(frontend): fix minor issues in the UI (#6382)
* fix flow result overflow

* fix resource type misalignment and icon sizes

* Do not display resource type description if empty

* Display flow yaml editor full height

* fix detail page overflow

* nit
2025-08-13 17:19:32 +00:00
Ruben Fiszel 1892895cd9 disable process group for dotnets 2025-08-13 16:46:14 +00:00
Ruben Fiszel 67381436a5 fix dotnets process group 2025-08-13 14:40:31 +00:00
Ruben Fiszel a4be29c34d fix dotnets process group 2025-08-13 12:14:03 +00:00
Ruben Fiszel f8ba3d1a1b disable process group optionally 2025-08-13 11:23:44 +00:00
Ruben Fiszel ef14290265 fix: fix csharp build hanging 2025-08-13 10:10:46 +00:00
Ruben Fiszel 36b742b3ad migrate to state 2025-08-13 00:57:15 +00:00
Ruben Fiszel 3d6d697ed3 update vite + sveltekit + adapter static 2025-08-13 00:10:48 +00:00
f03a8d69c0 feat(cli): add better error handling with path logging for JSON parsing failures (#6370)
* feat(cli): add better error handling with path logging for JSON parsing failures

- Add try-catch blocks with path logging for all JSON.parse operations in ZipFSElement
- Log specific file paths for flow.yaml, app.yaml, script.yaml, and resource.yaml parsing failures
- Improve debugging experience by showing which file caused parse errors before re-throwing
- Addresses feedback in issue #6369 for better error handling in CLI sync command

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* feat(cli): extend error handling to cover extractInlineScripts and additional parsing operations

- Add try-catch blocks around extractInlineScriptsForFlows and extractInlineScriptsForApps calls
- Add error handling for yamlStringify operations in flow, app, script, and resource processing
- Add error handling for yamlParseContent operations in multiple locations
- Add error handling for JSON.parse operations in comparison logic
- All error handlers log the specific file path that caused the failure for better debugging

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* refactor(cli): remove unnecessary try-catch blocks around yamlStringify calls

yamlStringify operations cannot fail so the try-catch blocks were
unnecessary. Kept the essential error handling for operations that
can actually fail like extractInlineScripts, JSON.parse, and yamlParseContent.

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
2025-08-12 22:07:04 +00:00
Ruben Fiszel e1629f799d fix: fix resource type search when adding resources 2025-08-12 19:10:23 +00:00
Ruben Fiszelandrubenfiszel 735ca2f70f chore(main): release 1.524.0 (#6364)
* chore(main): release 1.524.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-12 12:25:20 +00:00
centdix 8ec4d615d2 feat(mcp): allow filtering by folder (#6366)
* allow filtering mcp by folder

* nit

* add error if wrong format
2025-08-12 12:21:16 +00:00
Ruben Fiszel 85a9c91895 fix: fix preprocessor usage in python 2025-08-12 12:14:50 +00:00
Ruben Fiszel 0dc84254fc fix(app): improve copy paste of tables with sub-components 2025-08-12 10:01:13 +00:00
pyranota 60dd969d6c chore: add rust client build check workflow (#6365)
* chore: add rust client build check workflow

Signed-off-by: pyranota <pyra@duck.com>

* check for .rs files in backend

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-08-11 18:50:16 +00:00
pyranota 0893ce103f fix: fix v1.523.0 rust sdk build (#6363)
Signed-off-by: pyranota <pyra@duck.com>
2025-08-11 19:58:40 +02:00
centdix 22c90ba090 nit(mcp): Add warning when no runnable found (#6362)
* display included tools

* add warning when no tools

* fix workspace usage

* remove dispatch usage

* nit

* cache results

* no state
2025-08-11 16:55:12 +00:00
Ruben Fiszelandrubenfiszel 8bcd555351 chore(main): release 1.523.0 (#6359)
* chore(main): release 1.523.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-11 16:16:16 +00:00
Alexander Petric 1112de87d4 fix: add extra query params for token request for client credentials (#6360)
ee repo ref
2025-08-11 15:46:55 +00:00
Ruben Fiszel 322f68053a fix: improve app decision tree behavior 2025-08-11 15:29:17 +00:00
Ruben Fiszel 1fef46027b remove debug numbers 2025-08-11 12:21:15 +00:00
Ruben Fiszel c27df6a917 fix: remove spurrious error log for apps 2025-08-11 12:12:35 +00:00
Ruben Fiszel 13bf33f83c fix: improve app component loading speed 2025-08-11 12:09:45 +00:00
centdix 2471c7acad feat(aichat): add api mode to call api endpoints (#6343)
* add api mode

* add endpoint to list tools

* use new endpoint from frontend

* draft tool exec display

* cleaning

* improve claude.md

* better theming

* show actual data

* add bacon to gitignore

* simpler logic

* add openapi def

* cleaning

* add confirmation

* simplify

* fix cancel

* fix build

* cleaning

* better logic

* path instructions

* add new endpoint

* cleaning

* fix

* cancel when creating new chat

* nits

* handle errors

* allow changing mode to api mode
2025-08-11 11:13:44 +00:00
centdix 49f6a3d979 feat(aichat): add gpt5 compatibility (#6358)
* add gpt5 compat

* add to list

* fix

* fix

* fix
2025-08-11 12:28:42 +02:00
Ruben Fiszel a47463e053 fix(app): improve carousel list recursive error 2025-08-11 10:04:09 +00:00
Ruben Fiszel e134364afe chore(main): release 1.522.1 (#6357) 2025-08-11 06:19:42 +00:00
Ruben Fiszel 47c6386d0f fix(app): improve id handling for transformers 2025-08-11 06:17:47 +00:00
Ruben Fiszel 4ebea68d73 nits 2025-08-10 23:24:36 +00:00
Ruben Fiszel ea2f71d8be fix: improve validate ID for id editors 2025-08-10 23:14:16 +00:00
Ruben Fiszel 2648520b53 fix(app): handle inline script of components with underscore in apps 2025-08-10 23:10:15 +00:00
Ruben Fiszelandrubenfiszel 962465dd8b chore(main): release 1.522.0 (#6354)
* chore(main): release 1.522.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-10 22:24:57 +00:00
Ruben Fiszel eb5ac2594c sqlx 2025-08-08 22:05:53 +00:00
Ruben Fiszel 147e6975c4 feat: add configurable stale jobs detection and cancellation 2025-08-08 17:32:11 +00:00
Ruben Fiszel be6db04397 chore(main): release 1.521.0 (#6342) 2025-08-08 17:03:16 +00:00
Roderik-WU fcc2c080da Fix docstring example in load_s3_file_reader to use correct function name (#6349)
The usage example in the load_s3_file_reader docstring incorrectly showed `wmill.load_s3_file(...)`. Updated it to `wmill.load_s3_file_reader(...)` to match the actual method being documented.
2025-08-08 17:01:32 +00:00
dieriba ff08759a1a nits: remove unused (#6352)
* remove unused

* update repo ref
2025-08-08 16:57:04 +00:00
centdix c8fea3d34a add allowed bots (#6353) 2025-08-08 16:56:52 +00:00
Ruben Fiszel 2a6424672b fix: display if tag has an active workers attached to it in tag select 2025-08-08 16:40:48 +00:00
dieriba 49e6af0302 add ack_id field and update hub link for gcp (#6351) 2025-08-08 15:04:57 +00:00
windmill-internal-app[bot]andwindmill-internal-app[bot] 3cc69a03ac docs(changelog): add new entries from changelog (#6347)
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-08-08 13:29:04 +00:00
hugocasa 5a97258375 fix: do not save license key when renewing if the expiry date is earlier than that of the current key (#6346)
* fix: do not save license key when renewing if the expiry date is earlier than that of the current key

* tmp ref

* final ref
2025-08-08 14:23:31 +02:00
Ruben Fiszel 42e06e7feb fix oss build 2025-08-07 18:27:09 +00:00
Ruben Fiszel 0dd785e02a fix oss 2025-08-07 18:14:33 +00:00
dieriba 414f099188 feat: add instance-wide workspace prefix option for custom app (#6180) 2025-08-07 17:55:26 +00:00
Alexander PetricandRuben Fiszel 755e334303 feat: nextcloud oauth (#6341)
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-07 17:52:47 +00:00
dieribaandRuben Fiszel 852bf064dc feat: togglable manual acknowledgement for gcp trigger (#6321)
* update

* done

* update repo ref

* nits

* update repo ref

* fix

* all

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-07 17:45:50 +00:00
Ruben Fiszel 82aac9c666 nit tab change for streams result 2025-08-07 16:57:11 +00:00
Ruben Fiszelandrubenfiszel f31b276988 chore(main): release 1.520.1 (#6339)
* chore(main): release 1.520.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-07 16:28:14 +00:00
Ruben Fiszel 32b8e692b1 fix: fix oss build 2025-08-07 16:20:44 +00:00
Ruben Fiszelandrubenfiszel 9ac09d95e2 chore(main): release 1.520.0 (#6337)
* chore(main): release 1.520.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-07 15:59:10 +00:00
centdix 4d3746335e feat(mcp): add api endpoints as tools (#6329)
* working list tools

* working call tool

* add schema

* implement calling the endpoint

* use openapi instead

* correctly implement call_tool

* provide workspace from context

* cleaning

* add more endpoints

* remove resolved hack

* add missing properties description

* add list scripts and flows

* add instructions

* remove bacon.toml

* cleaning

* remove bacon.toml

* nit

* nit

* cleaning

* fix openapi file

* nit

* better error handling
2025-08-07 15:55:35 +00:00
centdix 398c0bb34e chore(cli): easier dev on cli (#6336)
* add ts extensions by default

* remove script

* add script to remove ext

* simpler scripts

* add readme

* add revert mode

* nit

* fix

* fix typos

* typo
2025-08-07 15:55:14 +00:00
Alexander Petric 616a1a7308 bun install with no cache option if env: BUN_NO_CACHE (#6338)
* bun install with no cache option if env: BUN_NO_CACHE

* lazy static
2025-08-07 15:54:57 +00:00
Ruben Fiszel 7b4d6e62f4 optimize further app endpoints 2025-08-07 15:48:47 +00:00
Ruben Fiszel 9e5a1cee0c fix: improve result stream query efficiency 2025-08-07 15:31:47 +00:00
dieriba 9fbb1992e5 feat: add email workspace error handler if smtp is configured (#6267)
* detecting trigger kind on job error

* v0 done

* done

* updat .sqlx and add mgrations

* fix: set back vs code settings and unused

* fix unused import

* add feature flag

* update ref

* nits

* nits

* done

* update repo ref

* remove

* add isCloudHosted

* update sqlx

* big

* fix

* use extra args instead of new col

* nits

* update .sqllx

* update ref

* update email

* fix

* fix

* fix

* nits

* update script and use env base_internal_url

* update sqlx and fix query

* remove

* fix unused import

* update ref
2025-08-07 16:46:53 +02:00
Ruben Fiszel 84f76eebf7 update openapi spec exposed docs 2025-08-07 12:06:22 +00:00
Ruben Fiszel eb9443ffc5 update openapi spec exposed docs 2025-08-07 11:52:06 +00:00
Ruben Fiszelandrubenfiszel d653644329 chore(main): release 1.519.2 (#6335)
* chore(main): release 1.519.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-07 07:10:43 +00:00
Ruben Fiszel 545d3ce74c fix: native text response streaming in agent workers 2025-08-07 06:56:56 +00:00
Ruben Fiszelandrubenfiszel 77debd1759 chore(main): release 1.519.1 (#6334)
* chore(main): release 1.519.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-06 23:21:07 +00:00
Ruben Fiszel fb9d7d4c95 fix: fix python-client f formatting error 2025-08-06 23:18:09 +00:00
316 changed files with 17580 additions and 6626 deletions
+7 -1
View File
@@ -46,7 +46,13 @@ jobs:
exit 0
fi
# 2. Otherwise fall back to the org-membership check
# 2. Disallow other bots
if [[ "${COMMENTER}" =~ \[bot\]$ ]]; then
echo "is_member=false" >> $GITHUB_OUTPUT
exit 0
fi
# 3. Otherwise check if the user is a member of the organization
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
+1
View File
@@ -98,6 +98,7 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
allowed_tools: "mcp__github__create_pull_request,Bash"
allowed_bots: "windmill-internal-app[bot]"
custom_instructions: |
## IMPORTANT INSTRUCTIONS
- Your branch name should be a short description of the requested changes.
+27
View File
@@ -0,0 +1,27 @@
name: Rust Client Check
on:
workflow_run:
workflows: ["Change versions"]
types:
- completed
push:
paths:
- "rust-client/**"
- "backend/**/*.rs"
- "backend/windmill-api/openapi.yaml"
- "version.txt"
- "flake.nix"
- ".github/workflows/rust-client-check.yml"
jobs:
check_rust_client:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Check rust client builds
run: cd rust-client && nix develop ../ --command ./dev.nu --check
timeout-minutes: 16
+113
View File
@@ -1,5 +1,118 @@
# Changelog
## [1.525.0](https://github.com/windmill-labs/windmill/compare/v1.524.0...v1.525.0) (2025-08-14)
### Features
* **aichat:** add test tool to script and flow mode ([#6367](https://github.com/windmill-labs/windmill/issues/6367)) ([34773f2](https://github.com/windmill-labs/windmill/commit/34773f2614450d0e82b190c04bb446dec74f84dc))
* **cli:** add better error handling with path logging for JSON parsing failures ([#6370](https://github.com/windmill-labs/windmill/issues/6370)) ([f03a8d6](https://github.com/windmill-labs/windmill/commit/f03a8d69c017e5ac8bb34cabdfd5c634dc126f3f))
* **frontend:** add flow log view ([#6330](https://github.com/windmill-labs/windmill/issues/6330)) ([4ec1dce](https://github.com/windmill-labs/windmill/commit/4ec1dce5313177079177b9d90558e8085599d19d))
### Bug Fixes
* fix csharp build hanging ([ef14290](https://github.com/windmill-labs/windmill/commit/ef14290265eaf327d3e42b7f2fbb9dfd9eb3a873))
* fix resource type search when adding resources ([e1629f7](https://github.com/windmill-labs/windmill/commit/e1629f799d60b4ca5db1e469cac57cc6cfc7d83f))
* **frontend:** do not open popup when clicking on wand in flow inline script editor ([#6374](https://github.com/windmill-labs/windmill/issues/6374)) ([2b37281](https://github.com/windmill-labs/windmill/commit/2b372810844cd28019145c1a825dd0ac6e924292))
* **frontend:** fix minor issues in the UI ([#6382](https://github.com/windmill-labs/windmill/issues/6382)) ([a41edd2](https://github.com/windmill-labs/windmill/commit/a41edd236bdd3196468cdf2586c95ff0a4c1abf5))
## [1.524.0](https://github.com/windmill-labs/windmill/compare/v1.523.0...v1.524.0) (2025-08-12)
### Features
* **mcp:** allow filtering by folder ([#6366](https://github.com/windmill-labs/windmill/issues/6366)) ([8ec4d61](https://github.com/windmill-labs/windmill/commit/8ec4d615d251a0a2ed26f3b1907d6f813d91f43c))
### Bug Fixes
* **app:** improve copy paste of tables with sub-components ([0dc8425](https://github.com/windmill-labs/windmill/commit/0dc84254fc152df82ffbd137f80ed01225c00043))
* fix preprocessor usage in python ([85a9c91](https://github.com/windmill-labs/windmill/commit/85a9c91895d0460e5e7d2d9ff0e53d05e2354386))
* fix v1.523.0 rust sdk build ([#6363](https://github.com/windmill-labs/windmill/issues/6363)) ([0893ce1](https://github.com/windmill-labs/windmill/commit/0893ce103ffcfbcb037c9e2ab851e480b61b3735))
## [1.523.0](https://github.com/windmill-labs/windmill/compare/v1.522.1...v1.523.0) (2025-08-11)
### Features
* **aichat:** add api mode to call api endpoints ([#6343](https://github.com/windmill-labs/windmill/issues/6343)) ([2471c7a](https://github.com/windmill-labs/windmill/commit/2471c7acad3404dd37649ed661ab8793607b5a97))
* **aichat:** add gpt5 compatibility ([#6358](https://github.com/windmill-labs/windmill/issues/6358)) ([49f6a3d](https://github.com/windmill-labs/windmill/commit/49f6a3d979ab8933de197dc0065c2720131fd597))
### Bug Fixes
* add extra query params for token request for client credentials ([#6360](https://github.com/windmill-labs/windmill/issues/6360)) ([1112de8](https://github.com/windmill-labs/windmill/commit/1112de87d440db5c8d0ddab6e3812dc432a58705))
* **app:** improve carousel list recursive error ([a47463e](https://github.com/windmill-labs/windmill/commit/a47463e05398e4ebe0f2d09ee2eba462bcef217b))
* improve app component loading speed ([13bf33f](https://github.com/windmill-labs/windmill/commit/13bf33f83c6660e05b78a8c941c2adc1b486f810))
* improve app decision tree behavior ([322f680](https://github.com/windmill-labs/windmill/commit/322f68053a238d6ef7c0116ccfb1a49a19e08f76))
* remove spurrious error log for apps ([c27df6a](https://github.com/windmill-labs/windmill/commit/c27df6a917d8a585593de0183f7ff1c5bb2a0321))
## [1.522.1](https://github.com/windmill-labs/windmill/compare/v1.522.0...v1.522.1) (2025-08-11)
### Bug Fixes
* **app:** handle inline script of components with underscore in apps ([2648520](https://github.com/windmill-labs/windmill/commit/2648520b53925616b02ecab060e4d2d6db8c2e34))
* **app:** improve id handling for transformers ([47c6386](https://github.com/windmill-labs/windmill/commit/47c6386d0ff6d59367ee38e0704d0e98802e1bff))
* improve validate ID for id editors ([ea2f71d](https://github.com/windmill-labs/windmill/commit/ea2f71d8be424fe13772ec1b7eba85d55bc4eae4))
## [1.522.0](https://github.com/windmill-labs/windmill/compare/v1.521.0...v1.522.0) (2025-08-08)
### Features
* add configurable stale jobs detection and cancellation ([147e697](https://github.com/windmill-labs/windmill/commit/147e6975c4b1e6c63e7b6b77c6645f8c88f0f78b))
## [1.521.0](https://github.com/windmill-labs/windmill/compare/v1.520.1...v1.521.0) (2025-08-08)
### Features
* add instance-wide workspace prefix option for custom app ([#6180](https://github.com/windmill-labs/windmill/issues/6180)) ([414f099](https://github.com/windmill-labs/windmill/commit/414f09918856eb1d577eb7776273b6697d11e848))
* nextcloud oauth ([#6341](https://github.com/windmill-labs/windmill/issues/6341)) ([755e334](https://github.com/windmill-labs/windmill/commit/755e3343035402b5993a516e58d03c10c47c3a00))
* togglable manual acknowledgement for gcp trigger ([#6321](https://github.com/windmill-labs/windmill/issues/6321)) ([852bf06](https://github.com/windmill-labs/windmill/commit/852bf064dc4f640dab4248082afade6eae8bb2cc))
### Bug Fixes
* display if tag has an active workers attached to it in tag select ([2a64246](https://github.com/windmill-labs/windmill/commit/2a6424672b5ed6adb1a408ddc8acbf7d7b2221ac))
* do not save license key when renewing if the expiry date is earlier than that of the current key ([#6346](https://github.com/windmill-labs/windmill/issues/6346)) ([5a97258](https://github.com/windmill-labs/windmill/commit/5a97258375d76164ed17f7b258fa9b3222459fe1))
## [1.520.1](https://github.com/windmill-labs/windmill/compare/v1.520.0...v1.520.1) (2025-08-07)
### Bug Fixes
* fix oss build ([32b8e69](https://github.com/windmill-labs/windmill/commit/32b8e692b1e8ba549b75a72ff17300aca6f38f8e))
## [1.520.0](https://github.com/windmill-labs/windmill/compare/v1.519.2...v1.520.0) (2025-08-07)
### Features
* add email workspace error handler if smtp is configured ([#6267](https://github.com/windmill-labs/windmill/issues/6267)) ([9fbb199](https://github.com/windmill-labs/windmill/commit/9fbb1992e505d47cbb3750db9c672e69b0d5f8e7))
* **mcp:** add api endpoints as tools ([#6329](https://github.com/windmill-labs/windmill/issues/6329)) ([4d37463](https://github.com/windmill-labs/windmill/commit/4d3746335e5f9056dafdb0557169f8fae5449005))
### Bug Fixes
* improve result stream query efficiency ([9e5a1ce](https://github.com/windmill-labs/windmill/commit/9e5a1cee0cc833d33d3b654cf330723295c3f31a))
## [1.519.2](https://github.com/windmill-labs/windmill/compare/v1.519.1...v1.519.2) (2025-08-07)
### Bug Fixes
* native text response streaming in agent workers ([545d3ce](https://github.com/windmill-labs/windmill/commit/545d3ce74c1edcf588289dc67aa4c365dd48ed10))
## [1.519.1](https://github.com/windmill-labs/windmill/compare/v1.519.0...v1.519.1) (2025-08-06)
### Bug Fixes
* fix python-client f formatting error ([fb9d7d4](https://github.com/windmill-labs/windmill/commit/fb9d7d4c95601965a0c00fe07458ea1b77a0fe69))
## [1.519.0](https://github.com/windmill-labs/windmill/compare/v1.518.2...v1.519.0) (2025-08-06)
+3 -1
View File
@@ -7,4 +7,6 @@ heaptrack*
index/
windmill-api/openapi-*.*
.duckdb/*
*ee.rs
*ee.rs
generate_mcp_endpoints_tools/venv
bacon.toml
@@ -147,6 +147,16 @@
"ordinal": 28,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 30,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -183,6 +193,8 @@
true,
true,
false,
true,
true,
true
]
},
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) \n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow \n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0a56301b5aaf57339cb2904c8f617366b74e891034d32f2867ccb019da869fc8"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator, added_via) VALUES ($1, $2, $3, false, $4, $5) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
@@ -8,10 +8,11 @@
"Varchar",
"Varchar",
"Varchar",
"Bool"
"Bool",
"Jsonb"
]
},
"nullable": []
},
"hash": "e822d186203fe809b764007ad7c02870a3b7d93ae43b40ac2cd3181dffab0837"
"hash": "0d7ce0397ef15c9d6cdaeaa2730a9e27fb7387ca24980df1481e6a94622ef006"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a"
}
@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
null
]
},
"hash": "10f6d3ffd7406146572b1becdce5c8da5242b58f6ce46ab10296cff9d6a3a6c4"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n path,\n custom_path\n FROM \n app\n WHERE \n custom_path IN (\n SELECT \n custom_path\n FROM \n app\n GROUP \n BY custom_path\n HAVING COUNT(*) > 1\n )\n ORDER BY custom_path\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "custom_path",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true
]
},
"hash": "11e24f758a70cd5f3a240bc81a05f40754826db0ee1194409227597a98603e92"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag::text, EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> ARRAY[tag] AND ping_at > now() - interval '1 minute') as exists\n FROM unnest($1::text[]) as tag",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": [
null,
null
]
},
"hash": "18ca698813b58c7f93139b12818681f9e36e8bb69b7a37755dc9b90c5e1af4cf"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE(auto_add_instance_groups, '{}') FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "coalesce",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "18e550f4ec23d465632449b88c4b25931f145f771b93828e8e6dfcc1f906443d"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET \n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6, \n edited_by = $7, \n email = $8,\n script_path = $9,\n path = $10,\n enabled = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE \n workspace_id = $12 AND \n path = $13\n ",
"query": "\n UPDATE \n gcp_trigger \n SET \n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6, \n edited_by = $7, \n email = $8,\n script_path = $9,\n path = $10,\n enabled = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17\n WHERE \n workspace_id = $12 AND \n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -30,10 +30,11 @@
"Text",
"Varchar",
"Jsonb",
"Jsonb"
"Jsonb",
"Bool"
]
},
"nullable": []
},
"hash": "566823041170df3a6379ce58d321895746b0cd8a6891b05d51abeb7079ded5d7"
"hash": "1a9ba16c90d3d65c4ff39aaddb3079009e03af711e7f6b53332537cf4cb0e8dd"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n ig.name as group_name,\n ws.workspace_id,\n w.name as workspace_name,\n ws.auto_add_instance_groups_roles->ig.name as role\n FROM instance_group ig\n INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL\n AND ig.name = ANY(ws.auto_add_instance_groups)\n INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false\n ORDER BY ig.name, ws.workspace_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "group_name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "workspace_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
null
]
},
"hash": "1e6c125c884002a1565b11a2c308ce89f9726d21bbb3cc712ad0b6450cbb44e6"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM email_to_igroup WHERE igroup = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "21099fabde943edc90d3a0125e8490691b2701d11f918da3bb5ae610d5c023a0"
}
@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT username, added_via\n FROM usr\n WHERE workspace_id = $1 AND email = $2\n AND added_via->>'source' = 'instance_group'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "added_via",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "2132a715995f2775917c59c01ba66f2e472a2347f25cc7109f36a805806ee6e2"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_add_instance_groups = NULL, auto_add_instance_groups_roles = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "31db242523c534b2abbb5f4985f56dd4493d19611ed988d694d9f4289dc29c48"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"describe": {
"columns": [
{
@@ -127,6 +127,11 @@
"ordinal": 20,
"name": "retry: _",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"name": "auto_acknowledge_msg",
"type_info": "Bool"
}
],
"parameters": {
@@ -153,8 +158,9 @@
false,
true,
true,
true,
true
]
},
"hash": "9aa094ef0652f36d473866469e71cc41c7b1cf848d0b4beffa7eefc0fbf9d64f"
"hash": "33aaf2dd14397d0b50b986ed55bc458337b85b6a9a9ab3d93d8f33d0f57e4b0f"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n email = $9,\n can_return_message = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"JsonbArray",
"JsonbArray",
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "35b211d19e53da4b64b0bd097284de3236ab939e47a1fc2b15ffc9607b552f8d"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n auto_add_instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_add_instance_groups IS NOT NULL\n AND $1 = ANY(auto_add_instance_groups)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "3ebc91867be0a4830ef4dbbe833aed348f78e3cb68fb1f3c855a491bdcda5017"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM postgres_trigger\n WHERE workspace_id = $1",
"query": "\n SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n postgres_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -120,5 +120,5 @@
true
]
},
"hash": "bf1fba73d3109aae48bda524cfafdf7f5b9b0e56396aed1b83a2c235222e8983"
"hash": "3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5"
}
@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET kafka_resource_path = $1, group_id = $2, topics = $3, script_path = $4, path = $5, is_flow = $6, edited_by = $7, email = $8, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $11, error_handler_args = $12, retry = $13\n WHERE workspace_id = $9 AND path = $10",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "3f91433fc076f47dd3516b40c9337981c329ddc31132882f037eaf2709aa8805"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3fdfcab1a54c166b1d8d43215d61268a251160db4630f0342522091668f36af0"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_add_instance_groups = $2, auto_add_instance_groups_roles = $3 WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"TextArray",
"Jsonb"
]
},
"nullable": []
},
"hash": "404233e74aaafd987879c6c87d1fd80928e6fade09e60a9a2d35121c81885cca"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"JsonbArray",
"JsonbArray",
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "4479beaefc36afba093e5113bb330a87979623d69b94f274863318ed351a1dac"
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT workspace_id, username, email\n FROM usr\n WHERE email = $1\n AND added_via->>'source' = 'instance_group'\n AND added_via->>'group' = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "472f224dc9e17d2c50cb9db13c34cc1fb9adb6d0ea36cf223541adb7cac17bdd"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n kafka_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
@@ -121,5 +121,5 @@
true
]
},
"hash": "6489342f52a9e1e90466bad48e0a6079fcabd696b02d57e3dbe652d28b84f008"
"hash": "494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET\n auto_add_instance_groups = array_remove(auto_add_instance_groups, $1),\n auto_add_instance_groups_roles = auto_add_instance_groups_roles - $1\n WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "4a446b97cf813226d25ca40cefca2999cdcf938f817327e9168b3edb0fce7fb7"
}
@@ -223,7 +223,8 @@
"ui",
"postgres",
"sqs",
"gcp"
"gcp",
"mqtt"
]
}
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM kafka_trigger\n WHERE workspace_id = $1",
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
@@ -120,5 +120,5 @@
true
]
},
"hash": "8f511fd837a99fa37a95781e50f8a83eaf93651f82c36c9795637d3ad761d003"
"hash": "4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8"
}
@@ -0,0 +1,36 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\", \n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", \n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM (\n SELECT $2::uuid as job_id, $1::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "stream_offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4"
]
},
"nullable": [
null,
null,
null
]
},
"hash": "506f90984a8b672aeff50b6d9e4751ea1521c524b9b7f51a48ee26280825ac9a"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT usage.usage FROM usage \n WHERE is_workspace = false \n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"query": "\n SELECT usage.usage FROM usage\n WHERE is_workspace = false\n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "a7b028e832396ee4d1ad6dfd44ba6134344f3eb37dbf0254154eba31b9cc2ed3"
"hash": "51cfe6efb154934f8cfbe77a7313891ef5c46a7febe4125d164a5ae5b55af2d6"
}
@@ -147,6 +147,16 @@
"ordinal": 28,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 30,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -183,6 +193,8 @@
true,
true,
false,
true,
true,
true
]
},
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE nats_trigger SET nats_resource_path = $1, subjects = $2, stream_name = $3, consumer_name = $4, use_jetstream = $5, script_path = $6, path = $7, is_flow = $8, edited_by = $9, email = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "570a4684bbdb390e4652301aa8d2d09b6e09b258f748d48cf2d188465621ee04"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> $1 AND ping_at > now() - interval '1 minute')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": [
null
]
},
"hash": "5d1ec728380ea8baf64df54743e73008ee9a58e6f39a5bf31a2ed099727f5c04"
}
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "added_via",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -57,6 +62,7 @@
false,
false,
false,
true,
true
]
},
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "added_via",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -58,6 +63,7 @@
false,
false,
false,
true,
true
]
},
@@ -45,11 +45,16 @@
},
{
"ordinal": 8,
"name": "added_via",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "super_admin",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "name",
"type_info": "Varchar"
}
@@ -69,6 +74,7 @@
false,
false,
true,
true,
false,
true
]
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f \n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f\n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
"describe": {
"columns": [
{
@@ -21,5 +21,5 @@
null
]
},
"hash": "388d6fd335a3f8a405b2d465892cf21a68d4b50ace25ef88c4cdf5b347c3d5eb"
"hash": "6b2820973026b1628cb93fda943bc389a261a3bb975c86ac02dd1c552da92295"
}
@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by AS \"created_by!\", coalesce(job_logs.logs, '') as logs\n FROM v2_job\n LEFT JOIN job_logs ON job_logs.job_id = v2_job.id\n WHERE v2_job.id = $1 AND v2_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_job.tag = ANY($3))\n ORDER BY job_logs.log_offset DESC\n LIMIT 100",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "logs",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"TextArray"
]
},
"nullable": [
false,
null
]
},
"hash": "6df2ca52e3e8515c398943da701600a33e64525809fdd67bce63e28d98eababc"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT usr.username\n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "6f36c05e6097066441acab227c0d5206d5a9a9828b04e4c107f1ecab4a9363d4"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_add_instance_groups IS NOT NULL AND $1 = ANY(auto_add_instance_groups)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "711c72a72298bb656882b35071e910dab721cda070f9c39bba3729bdc1467496"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM nats_trigger\n WHERE workspace_id = $1",
"query": "\n SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n nats_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -132,5 +132,5 @@
true
]
},
"hash": "5af0299841a0313d0cc270e98b04b5b5968184ae53dc076b1a9bc76af35923da"
"hash": "711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at \n ) usage\n WHERE workspace_id = $1\n ",
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at\n ) usage\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -24,5 +24,5 @@
null
]
},
"hash": "bff39cc57aba0729ddef1d53f3806c6736556f0a14b489d6708f9879393f9ea3"
"hash": "89940a53f29b173b6a8717f057a81a4f2edad152581950fdd80d758a0d242c17"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM http_trigger\n WHERE workspace_id = $1\n ",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n http_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -183,5 +183,5 @@
true
]
},
"hash": "09f43081a8b253d059d9d4302daa139cee6f055cdbafc213b2f992667f8ed578"
"hash": "89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE usr SET is_admin = true WHERE workspace_id = $1 AND email = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "94feb54cc965f19bfaf09966fd431b925c565f08c100bea812d3ac0e28664ccb"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT email FROM usr WHERE added_via->>'source' = 'instance_group' AND added_via->>'group' = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9530b234b2ac8f7360552be5e6bb25270e6b4185ce5c7f3c6edd9fbaabf77544"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE\n workspace_id = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Json",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "97b6e5779661343587c297b275df9e75fd22924ad2dbe124bb361f3e7fd8bfe0"
}
@@ -1,36 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM v2_job_completed FULL OUTER JOIN job_result_stream rs ON rs.job_id = v2_job_completed.id WHERE (v2_job_completed.id = $2 AND v2_job_completed.workspace_id = $1 OR rs.workspace_id = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "stream_offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4"
]
},
"nullable": [
true,
null,
null
]
},
"hash": "9f5b677a02690d3e4b4a5f5e141c7107077bbe90423102b5469e219f2a8b9293"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, ducklake, git_sync, deploy_ui, default_app, default_scripts, mute_critical_alerts, color, operator_settings, git_app_installations FROM workspace_settings WHERE workspace_id = $1",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_add_instance_groups,\n auto_add_instance_groups_roles\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -147,6 +147,16 @@
"ordinal": 28,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 30,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -183,8 +193,10 @@
true,
true,
true,
false
false,
true,
true
]
},
"hash": "6e60068b582af46afbb93010355cb5e7cc038042a6fa7089f3f73cd137a63956"
"hash": "ac1fd12e9ec92022be38aee0e91e9002e0e810d0e76dce5ced1000a7cb514adb"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -98,10 +98,32 @@
"Bool",
"JsonbArray",
"TextArray",
"Uuid"
"Uuid",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
}
]
},
"nullable": []
},
"hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d"
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
false,
true
true,
false
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "b45e17ad532a23b394226c9a5d7ab5a21e20202dbbf9c67831cc62eb067cd2ba"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)\n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c269f14ae9ae4e96eff9483eb84cccbdfa316e5db051f1d52085a2d5447c81ae"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -127,6 +127,11 @@
"ordinal": 20,
"name": "retry: _",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"name": "auto_acknowledge_msg",
"type_info": "Bool"
}
],
"parameters": {
@@ -155,8 +160,9 @@
false,
true,
true,
true,
true
]
},
"hash": "e82b99583d5d3004b4ad6606e7906b8f95b33c6137f136edabb0255bfa6a98f7"
"hash": "c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e"
}
@@ -0,0 +1,42 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",\n jq.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM (\n SELECT $1::uuid as job_id, $2::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "running: Option<bool>",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "stream_offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int4"
]
},
"nullable": [
null,
false,
null,
null
]
},
"hash": "c61f9bad73beb0bc317594f6ca68389c2d0adbef1fb6d73ad23e4153cff44f9e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema \n FROM flow \n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema\n FROM flow\n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
"describe": {
"columns": [
{
@@ -42,5 +42,5 @@
true
]
},
"hash": "974c7e623f3dfa440e134eaaa8d029334c0645147200219c39b2c00b30941172"
"hash": "ccbf71572dfc60b69a1666d4f1a883ba724d1e9c3303df32b001737b80a4b9f9"
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2, error_handler_muted_on_cancel = $3 WHERE workspace_id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Json",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "d21e5be1ac26db926b7196316dae0c9fa82a865b95bb55de5370e4ee34889ba3"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d9308c0154e029f7568b1d021368ca07f4867d192aa58b3888ae532d1c040828"
}
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "added_via",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -57,6 +62,7 @@
false,
false,
false,
true,
true
]
},
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4,\n $5,\n $6, \n $7, \n $8, \n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15\n )",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4,\n $5,\n $6, \n $7, \n $8, \n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16\n )",
"describe": {
"columns": [],
"parameters": {
@@ -29,10 +29,11 @@
"Varchar",
"Varchar",
"Jsonb",
"Jsonb"
"Jsonb",
"Bool"
]
},
"nullable": []
},
"hash": "7399ff3f22cbf2a522b6123e8ebf101ec9d41b7c8465cc501e61a1f6117981a2"
"hash": "e619fa013528a6275f98e14ae1727c55b0d4f4a5e4ee87c29251042e2916f0a0"
}
@@ -1,42 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n result as \"result: sqlx::types::Json<Box<RawValue>>\",\n v2_job_queue.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM v2_job_completed FULL OUTER JOIN v2_job_queue USING (id) \n LEFT JOIN job_result_stream rs ON rs.job_id = $1\n WHERE (v2_job_queue.id = $1 AND v2_job_queue.workspace_id = $2) OR (v2_job_completed.id = $1 AND v2_job_completed.workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "running: Option<bool>",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "stream_offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int4"
]
},
"nullable": [
true,
false,
null,
null
]
},
"hash": "ec0f8fa36328507e51c1974dbef884b755504a6cefa4af34fa4659fb95a7ee9a"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT email_to_igroup.email\n FROM email_to_igroup\n INNER JOIN instance_group ON instance_group.name = email_to_igroup.igroup\n WHERE instance_group.name = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "ee182378d9a760c3593b483703ec1682488ca9e6e402ec41abaa6ae3ca263854"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
@@ -127,6 +127,11 @@
"ordinal": 20,
"name": "retry: _",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"name": "auto_acknowledge_msg",
"type_info": "Bool"
}
],
"parameters": {
@@ -156,8 +161,9 @@
false,
true,
true,
true,
true
]
},
"hash": "b13d1dec373e70ffd58001cd17e9853e76ee8527e46afbfbc303528e512ebed9"
"hash": "f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE usr SET added_via = $1 WHERE workspace_id = $2 AND email = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f582cac90b4b7d732956b74eebc51323ef8acd3f627e7517451fcc72998d22bc"
}
@@ -41,11 +41,11 @@
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true
]
},
+258 -258
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.519.0"
version = "1.525.0"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.519.0"
version = "1.525.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -77,7 +77,7 @@ mcp = ["windmill-api/mcp"]
mqtt_trigger = ["windmill-api/mqtt_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
gcp_trigger = ["windmill-api/gcp_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
license = ["windmill-api/license"]
oauth2 = ["windmill-api/oauth2"]
zip = ["windmill-api/zip"]
+1 -1
View File
@@ -1 +1 @@
bb3ec8ecf883cd13be33f81cba98dec745500f62
d04959121c6869d764e11e005676003fd2919629
@@ -0,0 +1,35 @@
## MCP Tools Generator
The `generate_mcp_tools.py` script parses the OpenAPI specification and generates Rust code for MCP (Model Context Protocol) tools.
### Setup
```bash
cd backend/generate_mcp_endpoints_tools
pip install -r requirements.txt
```
### Usage
```bash
python3 generate_mcp_tools.py
```
The script will:
1. Parse `backend/windmill-api/openapi.yaml`
2. Find all endpoints marked with `x-mcp-tool: true`
3. Generate `backend/windmill-api/src/mcp_tools.rs` with a const array of tools
### Adding MCP Tools
To mark an endpoint as an MCP tool, add `x-mcp-tool: true` to the operation in the OpenAPI spec. You can also add `x-mcp-instructions` to complete the description of the tool with instructions on how to correctly use the endpoint:
```yaml
/w/{workspace}/scripts/list:
get:
x-mcp-tool: true
x-mcp-instructions: you should call that with this or that arg
summary: list scripts in workspace
operationId: listScripts
# ... rest of endpoint definition
```
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
Script to parse the OpenAPI YAML file and generate Rust code with MCP tools.
Searches for endpoints tagged with 'x-mcp-tool: true' and creates a const array.
"""
import json
import sys
from pathlib import Path
from typing import Dict, List, Any, Optional
IMPORTS = """
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
"""
ENDPOINT_STRUCT = """
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EndpointTool {
pub name: Cow<'static, str>,
pub description: Cow<'static, str>,
pub instructions: Cow<'static, str>,
pub path: Cow<'static, str>,
pub method: Cow<'static, str>,
pub path_params_schema: Option<serde_json::Value>,
pub query_params_schema: Option<serde_json::Value>,
pub body_schema: Option<serde_json::Value>,
}
"""
def load_openapi_spec(file_path: str) -> Dict[str, Any]:
"""Load and parse the OpenAPI YAML specification."""
try:
import yaml
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
except ImportError:
print("PyYAML not found. Please install it with: pip install PyYAML", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
sys.exit(1)
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any]) -> tuple:
"""Extract separate schemas for path parameters, query parameters, and request body."""
path_params_schema = {
"type": "object",
"properties": {},
"required": []
}
query_params_schema = {
"type": "object",
"properties": {},
"required": []
}
body_schema = None
# Process parameters
for param in parameters:
# Resolve $ref if present
if '$ref' in param:
param = resolve_schema_refs(param, spec)
param_name = param.get('name', '')
param_schema = param.get('schema', {'type': 'string'})
param_required = param.get('required', False)
param_description = param.get('description', '')
param_in = param.get('in', 'query')
# Resolve any refs in the parameter schema
param_schema = resolve_schema_refs(param_schema, spec)
# Add description if available
if param_description:
param_schema = dict(param_schema)
param_schema['description'] = param_description
# Route to appropriate schema based on parameter location
if param_in == 'path':
# Skip 'workspace' path parameter as it's automatically provided by the MCP context
if param_name != 'workspace':
path_params_schema['properties'][param_name] = param_schema
if param_required:
path_params_schema['required'].append(param_name)
elif param_in == 'query':
query_params_schema['properties'][param_name] = param_schema
if param_required:
query_params_schema['required'].append(param_name)
# Process request body if present
if request_body:
body_schema = extract_request_body_schema(request_body, spec)
# Return None for empty schemas
path_params_schema = path_params_schema if path_params_schema['properties'] else None
query_params_schema = query_params_schema if query_params_schema['properties'] else None
return (path_params_schema, query_params_schema, body_schema)
def resolve_ref(ref_path: str, spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Resolve a $ref path to the actual schema definition."""
if not ref_path.startswith('#/'):
return None
# Remove the '#/' prefix and split by '/'
path_parts = ref_path[2:].split('/')
# Navigate through the spec following the path
current = spec
for part in path_parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current if isinstance(current, dict) else None
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively resolve all $ref references in a schema."""
if not isinstance(schema, dict):
return schema
# If this is a $ref, resolve it
if '$ref' in schema:
ref_path = schema['$ref']
resolved = resolve_ref(ref_path, spec)
if resolved:
# Recursively resolve any refs in the resolved schema
return resolve_schema_refs(resolved, spec)
else:
print(f"Warning: Could not resolve $ref: {ref_path}")
return schema
# Recursively process all values in the schema
resolved_schema = {}
for key, value in schema.items():
if isinstance(value, dict):
resolved_schema[key] = resolve_schema_refs(value, spec)
elif isinstance(value, list):
resolved_schema[key] = [
resolve_schema_refs(item, spec) if isinstance(item, dict) else item
for item in value
]
else:
resolved_schema[key] = value
return resolved_schema
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Extract request body schema from OpenAPI requestBody definition and resolve refs."""
if not request_body:
return None
content = request_body.get('content', {})
json_content = content.get('application/json', {})
schema = json_content.get('schema', {})
if schema:
# Resolve any $ref references in the schema
return resolve_schema_refs(schema, spec)
return None
def http_method_to_rust(method: str) -> str:
"""Convert HTTP method string to Rust http::Method enum."""
method_map = {
'get': 'http::Method::GET',
'post': 'http::Method::POST',
'put': 'http::Method::PUT',
'delete': 'http::Method::DELETE',
'patch': 'http::Method::PATCH',
'head': 'http::Method::HEAD',
'options': 'http::Method::OPTIONS'
}
return method_map.get(method.lower(), f'http::Method::{method.upper()}')
def schema_to_rust_value(schema: Optional[Dict[str, Any]]) -> str:
"""Convert a schema dict to a Rust serde_json::json! expression."""
if schema is None:
return "None"
return f"Some(serde_json::json!({json.dumps(schema, indent=8)}))"
def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Find all endpoints marked with x-mcp-tool: true."""
tools = []
paths = spec.get('paths', {})
for path, path_item in paths.items():
for method, operation in path_item.items():
if isinstance(operation, dict) and operation.get('x-mcp-tool') is True:
# Extract tool information
tool = {
'name': operation.get('operationId', f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"),
'description': operation.get('summary', operation.get('description', f'{method.upper()} {path}')),
'instructions': operation.get('x-mcp-instructions', ''),
'path': path,
'method': method.upper(),
'parameters': operation.get('parameters', []),
'requestBody': operation.get('requestBody'),
}
tools.append(tool)
return tools
def generate_rust_code(tools: List[Dict[str, Any]], spec: Dict[str, Any]) -> str:
"""Generate the complete Rust code with MCP tools."""
if not tools:
return """// No MCP tools found in the OpenAPI specification
{IMPORTS}
{ENDPOINT_STRUCT}
pub fn all_tools() -> Vec<EndpointTool> {
vec![]
}
"""
tool_definitions = []
for tool in tools:
tool_name = tool['name']
description = tool['description']
instructions = tool['instructions']
path = tool['path']
method = tool['method'].upper()
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec
)
path_params_rust = schema_to_rust_value(path_params_schema)
query_params_rust = schema_to_rust_value(query_params_schema)
body_schema_rust = schema_to_rust_value(body_schema)
# Generate tool definition
tool_def = f""" EndpointTool {{
name: Cow::Borrowed("{tool_name}"),
description: Cow::Borrowed("{description}"),
instructions: Cow::Borrowed("{instructions}"),
path: Cow::Borrowed("{path}"),
method: Cow::Borrowed("{method}"),
path_params_schema: {path_params_rust},
query_params_schema: {query_params_rust},
body_schema: {body_schema_rust},
}}"""
tool_definitions.append(tool_def)
# Combine everything
tool_definitions_str = ",\n".join(tool_definitions)
rust_code = f"""// Auto-generated MCP tools from OpenAPI specification
// This file is generated by generate_mcp_tools.py - DO NOT EDIT MANUALLY
{IMPORTS}
{ENDPOINT_STRUCT}
pub fn all_tools() -> Vec<EndpointTool> {{
vec![
{tool_definitions_str}
]
}}
"""
return rust_code
def main():
"""Main function to parse OpenAPI and generate Rust code."""
script_dir = Path(__file__).parent
backend_dir = script_dir.parent
openapi_file = backend_dir / "windmill-api" / "openapi.yaml"
output_file = backend_dir / "windmill-api" / "src" / "mcp" / "tools" / "auto_generated_endpoints.rs"
if not openapi_file.exists():
print(f"OpenAPI file not found: {openapi_file}", file=sys.stderr)
sys.exit(1)
print(f"Loading OpenAPI specification from: {openapi_file}")
spec = load_openapi_spec(str(openapi_file))
print("Searching for endpoints with x-mcp-tool: true...")
tools = find_mcp_tools(spec)
if tools:
print(f"Found {len(tools)} MCP tool(s):")
for tool in tools:
print(f" - {tool['name']}: {tool['method']} {tool['path']}")
else:
print("No MCP tools found (no endpoints with x-mcp-tool: true)")
print(f"Generating Rust code...")
rust_code = generate_rust_code(tools, spec)
print(f"Writing to: {output_file}")
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(rust_code)
print("Done!")
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
PyYAML>=6.0
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'mqtt';
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER table gcp_trigger DROP COLUMN auto_acknowledge_msg;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER table gcp_trigger ADD COLUMN auto_acknowledge_msg BOOLEAN DEFAULT true;
@@ -0,0 +1,4 @@
-- Remove auto-add columns for instance groups
ALTER TABLE workspace_settings
DROP COLUMN auto_add_instance_groups,
DROP COLUMN auto_add_instance_groups_roles;
@@ -0,0 +1,4 @@
-- Add auto-add columns for instance groups
ALTER TABLE workspace_settings
ADD COLUMN auto_add_instance_groups text[] DEFAULT '{}',
ADD COLUMN auto_add_instance_groups_roles jsonb DEFAULT '{}';
@@ -0,0 +1,3 @@
-- Remove added_via tracking
DROP INDEX idx_usr_added_via;
ALTER TABLE usr DROP COLUMN added_via;
@@ -0,0 +1,8 @@
-- Add added_via column to track how users were added to workspaces
-- NULL = manual addition
-- {"source": "domain", "domain": "company.com"} = domain auto-add
-- {"source": "instance_group", "group": "developers", "role": "developer"} = instance group auto-add
ALTER TABLE usr ADD COLUMN added_via jsonb DEFAULT NULL;
-- Add index for efficient queries on added_via
CREATE INDEX idx_usr_added_via ON usr USING gin (added_via);
View File
+20 -15
View File
@@ -36,16 +36,16 @@ use windmill_common::{
agent_workers::build_agent_http_client,
get_database_url,
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING,
CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING,
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING,
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
APP_WORKSPACED_ROUTE_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING,
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
HUB_BASE_URL_SETTING, INDEXER_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING,
OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
@@ -89,11 +89,11 @@ use windmill_worker::{
use crate::monitor::{
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
reload_job_default_timeout_setting, reload_jwt_secret_setting, reload_license_key,
reload_npm_config_registry_setting, reload_pip_index_url_setting,
reload_app_workspaced_route_setting, reload_base_url_setting,
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_setting,
reload_license_key, reload_npm_config_registry_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config, MonitorIteration,
};
@@ -1034,6 +1034,11 @@ Windmill Community Edition {GIT_VERSION}
if let Err(e) = load_metrics_debug_enabled(&conn).await {
tracing::error!(error = %e, "Could not reload debug metrics setting");
}
},
APP_WORKSPACED_ROUTE_SETTING => {
if let Err(e) = reload_app_workspaced_route_setting(&db).await {
tracing::error!(error = %e, "Could not reload app workspaced route setting");
}
},
OTEL_SETTING => {
tracing::info!("OTEL setting changed, restarting");
+106 -2
View File
@@ -11,7 +11,7 @@ use std::{
time::Duration,
};
use chrono::{NaiveDateTime, Utc};
use chrono::{DateTime, NaiveDateTime, Utc};
use futures::{stream::FuturesUnordered, StreamExt};
use serde::{de::DeserializeOwned, Deserialize};
use sqlx::{Pool, Postgres};
@@ -33,13 +33,13 @@ use windmill_common::ee_oss::low_disk_alerts;
#[cfg(feature = "enterprise")]
use windmill_common::ee_oss::{jobs_waiting_alerts, worker_groups_alerts};
use windmill_common::client::AuthedClient;
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::reload_object_store_setting;
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
apps::APP_WORKSPACED_ROUTE,
auth::create_token_for_owner,
ee_oss::CriticalErrorChannel,
error,
@@ -77,6 +77,7 @@ use windmill_common::{
METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED,
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
};
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
use windmill_queue::{cancel_job, MiniPulledJob, SameWorkerPayload};
use windmill_worker::{
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
@@ -140,6 +141,9 @@ lazy_static::lazy_static! {
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true");
static ref STALE_JOB_TRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_TRESHOLD_MINUTES")
.ok()
.and_then(|x| x.parse::<u64>().ok());
}
pub async fn initial_load(
@@ -236,6 +240,10 @@ pub async fn initial_load(
if let Err(e) = reload_custom_tags_setting(db).await {
tracing::error!("Error reloading custom tags: {:?}", e)
}
if let Err(e) = reload_app_workspaced_route_setting(db).await {
tracing::error!("Error reloading app workspaced route: {:?}", e)
}
}
#[cfg(feature = "parquet")]
@@ -1009,6 +1017,7 @@ pub async fn reload_timeout_wait_result_setting(conn: &Connection) {
)
.await;
}
pub async fn reload_saml_metadata_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
@@ -1371,6 +1380,14 @@ pub async fn monitor_db(
}
};
let stale_jobs_f = async {
if server_mode && !initial_load {
if let Some(db) = conn.as_sql() {
stale_job_cancellation(&db).await;
}
}
};
// run every 5 minutes
let cleanup_concurrency_counters_f = async {
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(10) {
@@ -1469,6 +1486,7 @@ pub async fn monitor_db(
join!(
expired_items_f,
zombie_jobs_f,
stale_jobs_f,
expose_queue_metrics_f,
verify_license_key_f,
worker_groups_alerts_f,
@@ -1711,6 +1729,67 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
Ok(())
}
async fn stale_job_cancellation(db: &Pool<Postgres>) {
if let Some(threshold) = *STALE_JOB_TRESHOLD_MINUTES {
let stale_jobs = sqlx::query!(
"SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
threshold.to_string()
)
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
if !stale_jobs.is_empty() {
tracing::info!(
"Cancelling {} stale jobs (> {} minutes old)",
stale_jobs.len(),
threshold
);
}
for job in stale_jobs {
if let Err(e) =
cancel_stale_job(db, job.id, job.tag, job.workspace_id, job.scheduled_for).await
{
tracing::error!("Error cancelling stale job {}: {}", job.id, e);
}
}
}
}
async fn cancel_stale_job(
db: &Pool<Postgres>,
id: Uuid,
tag: String,
workspace_id: String,
scheduled_for: DateTime<Utc>,
) -> error::Result<()> {
let mut tx = db.begin().await?;
tracing::error!(
"Stale job detected: {} in workspace {} with tag {} (scheduled for: {}) . Cancelling it.",
id,
workspace_id,
tag,
scheduled_for
);
(tx, _) = cancel_job(
"monitor",
Some(format!(
"Stale job cancellation (scheduled for: {})",
scheduled_for
)),
id,
&workspace_id,
tx,
db,
true,
false,
)
.await?;
tx.commit().await?;
Ok(())
}
const RESTART_LIMIT: i32 = 3;
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
@@ -2344,6 +2423,31 @@ pub async fn reload_critical_error_channels_setting(conn: &DB) -> error::Result<
Ok(())
}
pub async fn reload_app_workspaced_route_setting(conn: &DB) -> error::Result<()> {
let app_workspaced_route =
load_value_from_global_settings(conn, APP_WORKSPACED_ROUTE_SETTING).await?;
println!("Updating...");
let ws_route = match app_workspaced_route {
Some(serde_json::Value::Bool(ws_route)) => ws_route,
None => false,
_ => {
tracing::error!(
"Expected {} to be a boolean got: {:?}. Defaulting to false",
APP_WORKSPACED_ROUTE_SETTING,
app_workspaced_route
);
false
}
};
let mut l = APP_WORKSPACED_ROUTE.write().await;
*l = ws_route;
Ok(())
}
pub async fn reload_critical_alerts_on_db_oversize(conn: &DB) -> error::Result<()> {
#[derive(Deserialize)]
struct DBOversize {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+304 -12
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.519.0
version: 1.525.0
title: Windmill API
contact:
@@ -1944,6 +1944,14 @@ paths:
type: boolean
auto_add:
type: boolean
auto_add_instance_groups:
type: array
items:
type: string
auto_add_instance_groups_roles:
type: object
additionalProperties:
type: string
plan:
type: string
customer_id:
@@ -2351,6 +2359,39 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_instance_groups:
post:
summary: edit instance groups
operationId: editInstanceGroups
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: Instance Groups Configuration
required: true
content:
application/json:
schema:
type: object
properties:
groups:
type: array
items:
type: string
roles:
type: object
additionalProperties:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/edit_webhook:
post:
summary: edit webhook
@@ -2440,7 +2481,6 @@ paths:
$ref: "#/components/schemas/ScriptArgs"
error_handler_muted_on_cancel:
type: boolean
responses:
"200":
description: status
@@ -2474,7 +2514,6 @@ paths:
application/json:
schema: {}
/w/{workspace}/workspaces/list_ducklakes:
get:
summary: list ducklakes
@@ -3069,11 +3108,13 @@ paths:
post:
summary: create variable
operationId: createVariable
x-mcp-tool: true
tags:
- variable
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: already_encrypted
description: whether the variable is already encrypted (default false)
in: query
schema:
type: boolean
@@ -3119,6 +3160,7 @@ paths:
delete:
summary: delete variable
operationId: deleteVariable
x-mcp-tool: true
tags:
- variable
parameters:
@@ -3136,12 +3178,14 @@ paths:
post:
summary: update variable
operationId: updateVariable
x-mcp-tool: true
tags:
- variable
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
- name: already_encrypted
description: whether the variable is already encrypted (default false)
in: query
schema:
type: boolean
@@ -3164,6 +3208,7 @@ paths:
get:
summary: get variable
operationId: getVariable
x-mcp-tool: true
tags:
- variable
parameters:
@@ -3228,11 +3273,13 @@ paths:
get:
summary: list variables
operationId: listVariable
x-mcp-tool: true
tags:
- variable
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: path_start
description: filter variables by path prefix
in: query
schema:
type: string
@@ -3833,11 +3880,13 @@ paths:
post:
summary: create resource
operationId: createResource
x-mcp-tool: true
tags:
- resource
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: update_if_exists
description: update the resource if it already exists (default false)
in: query
schema:
type: boolean
@@ -3860,6 +3909,7 @@ paths:
delete:
summary: delete resource
operationId: deleteResource
x-mcp-tool: true
tags:
- resource
parameters:
@@ -3877,6 +3927,7 @@ paths:
post:
summary: update resource
operationId: updateResource
x-mcp-tool: true
tags:
- resource
parameters:
@@ -3927,6 +3978,7 @@ paths:
get:
summary: get resource
operationId: getResource
x-mcp-tool: true
tags:
- resource
parameters:
@@ -3999,6 +4051,7 @@ paths:
get:
summary: list resources
operationId: listResource
x-mcp-tool: true
tags:
- resource
parameters:
@@ -4016,6 +4069,7 @@ paths:
schema:
type: string
- name: path_start
description: filter resources by path prefix
in: query
schema:
type: string
@@ -4198,6 +4252,7 @@ paths:
get:
summary: list resource_types
operationId: listResourceType
x-mcp-tool: true
tags:
- resource
parameters:
@@ -4651,6 +4706,7 @@ paths:
get:
summary: list all scripts
operationId: listScripts
x-mcp-tool: true
tags:
- script
parameters:
@@ -5026,6 +5082,7 @@ paths:
get:
summary: get script by path
operationId: getScriptByPath
x-mcp-tool: true
tags:
- script
parameters:
@@ -5616,6 +5673,7 @@ paths:
get:
summary: list all flows
operationId: listFlows
x-mcp-tool: true
tags:
- flow
parameters:
@@ -5797,6 +5855,7 @@ paths:
get:
summary: get flow by path
operationId: getFlowByPath
x-mcp-tool: true
tags:
- flow
parameters:
@@ -7247,6 +7306,7 @@ paths:
get:
summary: list all queued jobs
operationId: listQueue
x-mcp-tool: true
tags:
- job
parameters:
@@ -7586,6 +7646,7 @@ paths:
get:
summary: list all jobs
operationId: listJobs
x-mcp-tool: true
tags:
- job
parameters:
@@ -7757,6 +7818,10 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- name: remove_ansi_warnings
in: query
schema:
type: boolean
responses:
"200":
description: job details
@@ -7765,6 +7830,23 @@ paths:
schema:
type: string
/w/{workspace}/jobs_u/get_completed_logs_tail/{id}:
get:
summary: get completed job logs tail
operationId: getCompletedJobLogsTail
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
responses:
"200":
description: completed job logs tail
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs_u/get_args/{id}:
get:
summary: get job args
@@ -7857,6 +7939,10 @@ paths:
in: query
schema:
type: integer
- name: stream_offset
in: query
schema:
type: integer
- name: get_progress
in: query
schema:
@@ -8542,6 +8628,11 @@ paths:
post:
summary: create schedule
operationId: createSchedule
x-mcp-tool: true
x-mcp-instructions: |
Creates a new schedule.
The schedule should include seconds.
You should get the schema of the script or flow before creating the schedule to correctly specify the arguments needed.
tags:
- schedule
parameters:
@@ -8565,6 +8656,11 @@ paths:
post:
summary: update schedule
operationId: updateSchedule
x-mcp-tool: true
x-mcp-instructions: |
Updates a schedule.
The schedule should include seconds.
You should get the schema of the script or flow before updating the schedule to correctly specify the arguments needed.
tags:
- schedule
parameters:
@@ -8619,6 +8715,7 @@ paths:
delete:
summary: delete schedule
operationId: deleteSchedule
x-mcp-tool: true
tags:
- schedule
parameters:
@@ -8636,6 +8733,7 @@ paths:
get:
summary: get schedule
operationId: getSchedule
x-mcp-tool: true
tags:
- schedule
parameters:
@@ -8670,6 +8768,7 @@ paths:
get:
summary: list schedules
operationId: listSchedules
x-mcp-tool: true
tags:
- schedule
parameters:
@@ -8683,10 +8782,12 @@ paths:
schema:
type: string
- name: is_flow
description: filter schedules by whether they target a flow
in: query
schema:
type: boolean
- name: path_start
description: filter schedules by path prefix
in: query
schema:
type: string
@@ -10653,6 +10754,21 @@ paths:
type: array
items:
$ref: "#/components/schemas/InstanceGroup"
/groups/list_with_workspaces:
get:
summary: list instance groups with workspace information
operationId: listInstanceGroupsWithWorkspaces
tags:
- group
responses:
"200":
description: instance group list with workspaces
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/InstanceGroupWithWorkspaces"
/groups/get/{name}:
get:
@@ -11291,6 +11407,7 @@ paths:
get:
summary: list workers
operationId: listWorkers
x-mcp-tool: true
tags:
- worker
parameters:
@@ -11312,25 +11429,28 @@ paths:
items:
$ref: "#/components/schemas/WorkerPing"
/workers/exists_worker_with_tag:
/workers/exists_workers_with_tags:
get:
summary: exists worker with tag
operationId: existsWorkerWithTag
summary: exists workers with tags
operationId: existsWorkersWithTags
tags:
- worker
parameters:
- name: tag
- name: tags
in: query
required: true
description: comma separated list of tags
schema:
type: string
responses:
"200":
description: whether a worker with the tag exists
description: map of tags to whether at least one worker with the tag exists
content:
application/json:
schema:
type: boolean
type: object
additionalProperties:
type: boolean
/workers/queue_metrics:
get:
@@ -13339,6 +13459,24 @@ paths:
access_type:
$ref: "#/components/schemas/AssetUsageAccessType"
/mcp/w/{workspace}/list_tools:
get:
summary: list available MCP tools
operationId: listMcpTools
tags:
- mcp
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: list of MCP tools available for the workspace
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/EndpointTool"
components:
securitySchemes:
bearerAuth:
@@ -13372,6 +13510,7 @@ components:
name: publication
in: path
required: true
description: The name of the publication
schema:
type: string
VersionId:
@@ -13799,6 +13938,38 @@ components:
# -- INLINE END --
# Do not change line above
EndpointTool:
type: object
required: [name, description, instructions, path, method]
properties:
name:
type: string
description: The tool name/operation ID
description:
type: string
description: Short description of the tool
instructions:
type: string
description: Detailed instructions for using the tool
path:
type: string
description: API endpoint path
method:
type: string
description: HTTP method (GET, POST, etc.)
path_params_schema:
type: object
description: JSON schema for path parameters
nullable: true
query_params_schema:
type: object
description: JSON schema for query parameters
nullable: true
body_schema:
type: object
description: JSON schema for request body
nullable: true
AIProvider:
type: string
enum:
@@ -14122,6 +14293,7 @@ components:
ScriptArgs:
type: object
description: The arguments to pass to the script or flow
additionalProperties: {}
Input:
@@ -14467,6 +14639,10 @@ components:
type: array
items:
type: string
added_via:
nullable: true
allOf:
- $ref: "#/components/schemas/UserSource"
required:
- email
- username
@@ -14478,6 +14654,22 @@ components:
- folders
- folders_owners
UserSource:
type: object
properties:
source:
type: string
enum: [domain, instance_group, manual]
description: "How the user was added to the workspace"
domain:
type: string
description: "The domain used for auto-invite (when source is 'domain')"
group:
type: string
description: "The instance group name (when source is 'instance_group')"
required:
- source
UserUsage:
type: object
properties:
@@ -14624,18 +14816,25 @@ components:
properties:
path:
type: string
description: The path to the variable
value:
type: string
description: The value of the variable
is_secret:
type: boolean
description: Whether the variable is a secret
description:
type: string
description: The description of the variable
account:
type: integer
description: The account identifier
is_oauth:
type: boolean
description: Whether the variable is an OAuth variable
expires_at:
type: string
description: The expiration date of the variable
format: date-time
required:
- path
@@ -14648,12 +14847,16 @@ components:
properties:
path:
type: string
description: The path to the variable
value:
type: string
description: The new value of the variable
is_secret:
type: boolean
description: Whether the variable is a secret
description:
type: string
description: The new description of the variable
AuditLog:
type: object
@@ -14986,11 +15189,14 @@ components:
properties:
path:
type: string
description: The path to the resource
value: {}
description:
type: string
description: The description of the resource
resource_type:
type: string
description: The resource_type associated with the resource
required:
- path
- value
@@ -15001,9 +15207,14 @@ components:
properties:
path:
type: string
description: The path to the resource
description:
type: string
description: The new description of the resource
value: {}
resource_type:
type: string
description: The new resource_type to be associated with the resource
Resource:
type: object
@@ -15199,59 +15410,91 @@ components:
- success
- duration_ms
ErrorHandler:
type: string
enum:
- custom
- slack
- teams
- email
NewSchedule:
type: object
properties:
path:
type: string
description: The path where the schedule will be created
schedule:
type: string
description: The cron schedule to trigger the script or flow. Should include seconds.
timezone:
type: string
description: The timezone to use for the cron schedule
script_path:
type: string
description: The path to the script or flow to trigger
is_flow:
type: boolean
description: Whether the schedule is for a flow
args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow
enabled:
type: boolean
description: Whether the schedule is enabled
on_failure:
# a reference to a script path, flow path, or webhook (script/<path>, flow/<path>)
type: string
description: The path to the script or flow to trigger on failure
on_failure_times:
type: number
description: The number of times to retry on failure
on_failure_exact:
type: boolean
description: Whether the schedule should only run on the exact time
on_failure_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on failure
on_recovery:
type: string
description: The path to the script or flow to trigger on recovery
on_recovery_times:
type: number
description: The number of times to retry on recovery
on_recovery_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on recovery
on_success:
type: string
description: The path to the script or flow to trigger on success
on_success_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on success
ws_error_handler_muted:
type: boolean
description: Whether the WebSocket error handler is muted
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: The retry configuration for the schedule
no_flow_overlap:
type: boolean
description: Whether the schedule should not run if a flow is already running
summary:
type: string
description: The summary of the schedule
description:
type: string
description: The description of the schedule
tag:
type: string
description: The tag of the schedule
paused_until:
type: string
description: The date and time the schedule will be paused until
format: date-time
cron_version:
type: string
description: The version of the cron schedule to use (last is v2)
required:
- path
- schedule
@@ -15265,51 +15508,69 @@ components:
properties:
schedule:
type: string
description: The cron schedule to trigger the script or flow. Should include seconds.
timezone:
type: string
description: The timezone to use for the cron schedule
args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow
on_failure:
# a reference to a script path, flow path, or webhook (script/<path>, flow/<path>)
type: string
description: The path to the script or flow to trigger on failure
on_failure_times:
type: number
description: The number of times to retry on failure
on_failure_exact:
type: boolean
description: Whether the schedule should only run on the exact time
on_failure_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on failure
on_recovery:
type: string
description: The path to the script or flow to trigger on recovery
on_recovery_times:
type: number
description: The number of times to retry on recovery
on_recovery_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on recovery
on_success:
type: string
description: The path to the script or flow to trigger on success
on_success_extra_args:
$ref: "#/components/schemas/ScriptArgs"
description: The arguments to pass to the script or flow on success
ws_error_handler_muted:
type: boolean
description: Whether the WebSocket error handler is muted
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: The retry configuration for the schedule
no_flow_overlap:
type: boolean
description: Whether the schedule should not run if a flow is already running
summary:
type: string
description: The summary of the schedule
description:
type: string
description: The description of the schedule
tag:
type: string
description: The tag of the schedule
paused_until:
type: string
description: The date and time the schedule will be paused until
format: date-time
cron_version:
type: string
description: The version of the cron schedule to use (last is v2)
required:
- schedule
- timezone
- script_path
- is_flow
- args
TriggerExtraProperty:
@@ -16072,6 +16333,8 @@ components:
type: boolean
enabled:
type: boolean
auto_acknowledge_msg:
type: boolean
error_handler_path:
type: string
error_handler_args:
@@ -16624,6 +16887,8 @@ components:
InstanceGroup:
type: object
required:
- name
properties:
name:
type: string
@@ -16633,6 +16898,34 @@ components:
type: array
items:
type: string
InstanceGroupWithWorkspaces:
type: object
required:
- name
properties:
name:
type: string
summary:
type: string
emails:
type: array
items:
type: string
workspaces:
type: array
items:
$ref: "#/components/schemas/WorkspaceInfo"
WorkspaceInfo:
type: object
properties:
workspace_id:
type: string
workspace_name:
type: string
role:
type: string
required:
- name
@@ -16836,7 +17129,6 @@ components:
additionalProperties:
type: boolean
FlowMetadata:
type: object
properties:
+36 -16
View File
@@ -52,7 +52,7 @@ use std::str;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
apps::{AppScriptId, ListAppQuery},
apps::{AppScriptId, ListAppQuery, APP_WORKSPACED_ROUTE},
auth::TOKEN_PREFIX_LEN,
cache::{self, future::FutureCachedExt},
db::UserDB,
@@ -157,7 +157,7 @@ pub struct AppVersion {
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize, Deserialize, FromRow)]
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct AppWithLastVersion {
pub id: i64,
pub path: String,
@@ -182,7 +182,7 @@ pub struct AppWithLastVersionAndStarred {
}
#[cfg(feature = "enterprise")]
#[derive(Serialize, FromRow)]
#[derive(Debug, Serialize, FromRow)]
pub struct AppWithLastVersionAndWorkspace {
#[sqlx(flatten)]
#[serde(flatten)]
@@ -524,21 +524,36 @@ async fn get_app_w_draft(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by,
app.draft_only, draft.value as "draft"
from app
INNER JOIN app_version ON
app_version.id = app.versions[array_upper(app.versions, 1)]
LEFT JOIN draft ON
app.path = draft.path AND draft.workspace_id = $2 AND draft.typ = 'app'
WHERE app.path = $1 AND app.workspace_id = $2"#,
r#"
SELECT
app.id,
app.path,
app.summary,
app.versions,
app.policy,
app.custom_path,
app.extra_perms,
app_version.value,
app_version.created_at,
app_version.created_by,
app.draft_only,
draft.value AS "draft"
FROM app
INNER JOIN app_version
ON app_version.id = app.versions[array_upper(app.versions, 1)]
LEFT JOIN draft
ON app.path = draft.path
AND draft.workspace_id = $2
AND draft.typ = 'app'
WHERE app.path = $1
AND app.workspace_id = $2
"#,
)
.bind(path.to_owned())
.bind(&w_id)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let app = not_found_if_none(app_o, "App", path)?;
@@ -642,11 +657,13 @@ async fn custom_path_exists(
Extension(db): Extension<DB>,
Path((w_id, custom_path)): Path<(String, String)>,
) -> JsonResult<bool> {
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
let exists =
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None }
if *CLOUD_HOSTED || as_workspaced_route { Some(&w_id) } else { None }
)
.fetch_one(&db)
.await?.unwrap_or(false);
@@ -976,11 +993,12 @@ async fn create_app_internal<'a>(
}
if let Some(custom_path) = &app.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
if *CLOUD_HOSTED { Some(w_id) } else { None }
if *CLOUD_HOSTED || as_workspaced_route { Some(w_id) } else { None }
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
@@ -1272,6 +1290,7 @@ async fn update_app_internal<'a>(
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
use sql_builder::prelude::*;
let mut tx = user_db.clone().begin(&authed).await?;
let npath = if ns.policy.is_some()
|| ns.path.is_some()
|| ns.summary.is_some()
@@ -1311,6 +1330,7 @@ async fn update_app_internal<'a>(
if let Some(ncustom_path) = &ns.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
if ncustom_path.is_empty() {
sqlb.set("custom_path", "NULL");
@@ -1318,7 +1338,7 @@ async fn update_app_internal<'a>(
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
ncustom_path,
if *CLOUD_HOSTED { Some(w_id) } else { None },
if *CLOUD_HOSTED || as_workspaced_route { Some(w_id) } else { None },
path,
w_id
)
+1
View File
@@ -176,6 +176,7 @@ pub struct GcpTriggerConfig {
#[serde(flatten)]
pub create_update: Option<CreateUpdateConfig>,
pub topic_id: String,
pub auto_acknowledge_msg: Option<bool>,
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
+16
View File
@@ -854,6 +854,22 @@ impl From<ApiAuthed> for Authed {
}
}
impl From<Authed> for ApiAuthed {
fn from(value: Authed) -> Self {
Self {
email: value.email,
username: value.username,
is_admin: value.is_admin,
is_operator: value.is_operator,
groups: value.groups,
folders: value.folders,
scopes: value.scopes,
username_override: None, // Authed doesn't have this field, so default to None
token_prefix: value.token_prefix,
}
}
}
impl From<&ApiAuthed> for AuditAuthor {
fn from(value: &ApiAuthed) -> Self {
Self {
+13 -11
View File
@@ -2,22 +2,23 @@
#[allow(unused)]
pub use crate::gcp_triggers_ee::*;
use serde_json::value::RawValue;
use sqlx::prelude::FromRow;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use sqlx::types::Json as SqlxJson;
use windmill_common::worker::to_raw_value;
use windmill_common::triggers::TriggerKind;
use crate::trigger_helpers::TriggerJobArgs;
#[cfg(not(feature = "private"))]
use {
crate::db::{ApiAuthed, DB},
crate::trigger_helpers::TriggerJobArgs,
axum::{extract::Request, Router},
http::HeaderMap,
serde::{Deserialize, Serialize},
serde_json::value::RawValue,
sqlx::prelude::FromRow,
sqlx::types::Json as SqlxJson,
std::collections::HashMap,
windmill_common::db::UserDB,
windmill_common::worker::to_raw_value,
windmill_common::{
error::{Error as WindmillError, Result as WindmillResult},
triggers::TriggerKind,
utils::empty_as_none,
},
};
@@ -134,7 +135,6 @@ pub fn gcp_push_route_handler() -> Router {
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
#[cfg(not(feature = "private"))]
pub struct GcpTrigger {
pub gcp_resource_path: String,
pub subscription_id: String,
@@ -160,8 +160,10 @@ pub struct GcpTrigger {
pub error_handler_args: Option<SqlxJson<HashMap<String, Box<RawValue>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<SqlxJson<windmill_common::flows::Retry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_acknowledge_msg: Option<bool>,
}
#[cfg(not(feature = "private"))]
impl TriggerJobArgs<String> for GcpTrigger {
fn v1_payload_fn(payload: String) -> HashMap<String, Box<RawValue>> {
HashMap::from([("payload".to_string(), to_raw_value(&payload))])
@@ -170,4 +172,4 @@ impl TriggerJobArgs<String> for GcpTrigger {
fn trigger_kind() -> TriggerKind {
TriggerKind::Gcp
}
}
}
+82 -2
View File
@@ -44,6 +44,7 @@ pub fn workspaced_service() -> Router {
pub fn global_service() -> Router {
Router::new()
.route("/list", get(list_igroups))
.route("/list_with_workspaces", get(list_igroups_with_workspaces))
.route("/get/:name", get(get_igroup))
.route("/create", post(create_igroup))
.route("/update/:name", post(update_igroup))
@@ -187,7 +188,7 @@ pub async fn require_is_owner(
) -> Result<()> {
let is_owner = query_scalar!(
"SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])
AND value::boolean)",
username,
@@ -418,7 +419,7 @@ async fn get_group(
let group = not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
let members = sqlx::query_scalar!(
"SELECT usr.username
"SELECT usr.username
FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2
WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
name,
@@ -638,6 +639,21 @@ struct IGroup {
summary: Option<String>,
emails: Option<Vec<String>>,
}
#[derive(Serialize)]
struct IGroupWithWorkspaces {
name: String,
summary: Option<String>,
emails: Option<Vec<String>>,
workspaces: Vec<WorkspaceInfo>,
}
#[derive(Serialize, Clone)]
struct WorkspaceInfo {
workspace_id: String,
workspace_name: String,
role: String,
}
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
@@ -652,6 +668,70 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
return Ok(Json(groups));
}
async fn list_igroups_with_workspaces(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroupWithWorkspaces>> {
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
// Get all instance groups with their emails first
let groups = sqlx::query_as!(
IGroup,
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary"
)
.fetch_all(&mut *tx)
.await?;
// Get all workspace mappings for instance groups in a single query
let workspace_mappings = sqlx::query!(
r#"
SELECT
ig.name as group_name,
ws.workspace_id,
w.name as workspace_name,
ws.auto_add_instance_groups_roles->ig.name as role
FROM instance_group ig
INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL
AND ig.name = ANY(ws.auto_add_instance_groups)
INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false
ORDER BY ig.name, ws.workspace_id
"#
)
.fetch_all(&mut *tx)
.await?;
// Create a map of group_name -> Vec<WorkspaceInfo>
let mut workspaces_by_group: std::collections::HashMap<String, Vec<WorkspaceInfo>> = std::collections::HashMap::new();
for mapping in workspace_mappings {
let role = mapping.role
.and_then(|r| r.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "developer".to_string());
let workspace_info = WorkspaceInfo {
workspace_id: mapping.workspace_id.clone(),
workspace_name: mapping.workspace_name,
role,
};
workspaces_by_group
.entry(mapping.group_name)
.or_insert_with(Vec::new)
.push(workspace_info);
}
let mut result = Vec::new();
for group in groups {
let workspaces = workspaces_by_group.get(&group.name).cloned().unwrap_or_default();
result.push(IGroupWithWorkspaces {
name: group.name,
summary: group.summary,
emails: group.emails,
workspaces,
});
}
tx.commit().await?;
return Ok(Json(result));
}
async fn get_igroup(Path(name): Path<String>, Extension(db): Extension<DB>) -> JsonResult<IGroup> {
let group = sqlx::query_as!(
IGroup,
+32 -34
View File
@@ -12,7 +12,6 @@ use crate::{
users::fetch_api_authed,
utils::{check_scopes, non_empty_str, ExpiringCacheEntry},
};
use anyhow::anyhow;
use axum::response::Response;
use axum::{
@@ -197,38 +196,37 @@ async fn list_triggers(
) -> error::JsonResult<Vec<HttpTrigger>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
let mut sqlb = SqlBuilder::select_from("http_trigger")
.fields(&[
"workspace_id",
"path",
"route_path",
"route_path_key",
"workspaced_route",
"wrap_body",
"raw_string",
"script_path",
"summary",
"description",
"is_flow",
"http_method",
"edited_by",
"email",
"edited_at",
"extra_perms",
"is_async",
"authentication_method",
"static_asset_config",
"is_static_website",
"authentication_resource_path",
"error_handler_path",
"error_handler_args",
"retry",
])
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
let mut sqlb = SqlBuilder::select_from("http_trigger");
sqlb.fields(&[
"workspace_id",
"path",
"route_path",
"route_path_key",
"workspaced_route",
"wrap_body",
"raw_string",
"script_path",
"summary",
"description",
"is_flow",
"http_method",
"edited_by",
"email",
"edited_at",
"extra_perms",
"is_async",
"authentication_method",
"static_asset_config",
"is_static_website",
"authentication_resource_path",
"error_handler_path",
"error_handler_args",
"retry",
])
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page);
if let Some(path) = lst.path {
sqlb.and_where_eq("script_path", "?".bind(&path));
}
@@ -391,7 +389,7 @@ async fn create_trigger_inner(
new_http_trigger.is_static_website,
new_http_trigger.error_handler_path,
new_http_trigger.error_handler_args as _,
new_http_trigger.retry as _,
new_http_trigger.retry as _
)
.execute(&mut *tx)
.await?;
+317 -12
View File
@@ -16,6 +16,7 @@ use http::{HeaderMap, HeaderName};
use itertools::Itertools;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use serde_json::Value;
use sqlx::Pool;
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
@@ -24,6 +25,8 @@ use std::str::FromStr;
use std::time::Instant;
use tokio::io::AsyncReadExt;
use tower::ServiceBuilder;
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::auth::is_super_admin_email;
use windmill_common::auth::TOKEN_PREFIX_LEN;
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
@@ -33,6 +36,8 @@ use windmill_common::jobs::{
};
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
use windmill_common::variables::get_workspace_key;
@@ -66,7 +71,9 @@ use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::audit_oss::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::worker::to_raw_value;
use windmill_common::{
cache,
db::UserDB,
@@ -85,7 +92,7 @@ use windmill_common::{
use windmill_common::{
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL,
};
use windmill_queue::{
cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs,
@@ -234,6 +241,10 @@ pub fn workspaced_service() -> Router {
)
.route("/run/dependencies", post(run_dependencies_job))
.route("/run/flow_dependencies", post(run_flow_dependencies_job))
.route(
"/send_email_with_instance_smtp",
post(send_email_with_instance_smtp),
)
}
pub fn workspace_unauthed_service() -> Router {
@@ -261,6 +272,7 @@ pub fn workspace_unauthed_service() -> Router {
.route("/get_root_job_id/:id", get(get_root_job))
.route("/get/:id", get(get_job))
.route("/get_logs/:id", get(get_job_logs))
.route("/get_completed_logs_tail/:id", get(get_completed_job_logs_tail))
.route("/get_args/:id", get(get_args))
.route("/get_flow_debug_info/:id", get(get_flow_job_debug_info))
.route("/completed/get/:id", get(get_completed_job))
@@ -1037,6 +1049,240 @@ impl<'a> GetQuery<'a> {
}
}
#[cfg(all(feature = "smtp", feature = "enterprise"))]
async fn send_workspace_trigger_failure_email_notification(
db: &Pool<Postgres>,
w_id: &str,
job_id: &Uuid,
trigger_path: Option<&str>,
runnable_path: Option<&str>,
email_recipients: &Vec<String>,
error: &Value,
) -> Result<String, Error> {
let smtp_config = match load_smtp_config(db).await? {
Some(config) => config,
None => {
tracing::info!(
"SMTP not configured, skipping workspace trigger failure email notification"
);
return Err(anyhow::anyhow!(
"SMTP not configured, skipping workspace trigger failure email notification"
)
.into());
}
};
let runnable_path = runnable_path.as_deref().unwrap_or("Unknown");
let (trigger_kind, trigger_path) = if let Some(trigger_path) = trigger_path.as_deref() {
match trigger_path.split_once('/') {
Some((trigger_kind, trigger_path)) => {
tracing::debug!(
"Workspace trigger job {} is a {:?} trigger",
&job_id,
trigger_kind
);
(Some(trigger_kind), Some(trigger_path))
}
_ => (None, None),
}
} else {
(None, None)
};
let base_url = BASE_URL.read().await;
let job_url = format!("{}/run/{}?workspace={}", base_url, &job_id, w_id);
let trigger_kind_str = trigger_kind.unwrap_or("Unknown").to_string().to_uppercase();
let subject = format!(
"Windmill Job Failed: {} in workspace {}",
runnable_path, w_id
);
let error_details = serde_json::to_string_pretty(&error)
.unwrap_or_else(|_| format!("Unable to serialize error: {:?}", error));
let trigger_info = if trigger_kind.is_some() && trigger_path.is_some() {
format!(
r#"
<div class="section">
<span class="label">Trigger path:</span> {}
</div>
<div class="section">
<span class="label">Trigger Type:</span> {}
</div>"#,
trigger_path.unwrap(),
trigger_kind_str
)
} else {
String::new()
};
let email_title = if trigger_kind.is_some() {
format!("Windmill Trigger Job {} Failed", &job_id)
} else {
format!("Windmill Job {} Failed", &job_id)
};
let content = format!(
r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; color: #333; }}
h1 {{ color: #c0392b; }}
.section {{ margin-bottom: 20px; }}
.label {{ font-weight: bold; }}
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }}
a.button {{
display: inline-block;
padding: 10px 15px;
margin-top: 10px;
color: #fff;
background-color: #2980b9;
text-decoration: none;
border-radius: 5px;
}}
</style>
</head>
<body>
<h1>{}</h1>
<div class="section">
<span class="label">Workspace:</span> {}
</div>
{}
<div class="section">
<span class="label">Script/Flow Path:</span> {}
</div>
<div class="section">
<span class="label">Job ID:</span> {}
</div>
<div class="section">
<span class="label">Error Details:</span>
<pre>{}</pre>
</div>
<a href="{}" class="button">View Job Details</a>
</body>
</html>
"#,
email_title,
w_id,
trigger_info,
runnable_path,
&job_id,
error_details,
job_url
);
if let Err(e) = send_email_html(
&subject,
&content,
email_recipients.to_owned(),
smtp_config,
None,
)
.await
{
let err_msg = format!(
"Failed to send workspace email notification for trigger failure (trigger kind: {}, job ID: {}): {}",
trigger_kind_str,
&job_id,
e
);
return Err(Error::internal_err(err_msg));
}
let success_msg = format!(
"Job ID '{}' failed. An email with error details has been sent to {:?}.",
&job_id, email_recipients
);
tracing::info!("{}", &success_msg);
Ok(success_msg)
}
#[derive(Debug, Serialize, Deserialize)]
struct SendEmail {
job_id: Uuid,
#[serde(default, deserialize_with = "empty_as_none")]
trigger_path: Option<String>,
#[serde(default, deserialize_with = "empty_as_none")]
runnable_path: Option<String>,
#[serde(default, deserialize_with = "empty_as_none")]
email_recipients: Option<Vec<String>>,
error: Value,
}
#[cfg(all(feature = "enterprise", feature = "smtp"))]
async fn send_email_with_instance_smtp(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(send_email): Json<SendEmail>,
) -> error::Result<Json<String>> {
use windmill_common::jobs::EMAIL_ERROR_HANDLER_USER_EMAIL;
if *CLOUD_HOSTED {
tracing::warn!(
"Workspace trigger failure email notification is not available for cloud hosted Windmill",
);
return Err(anyhow::anyhow!("Feature not supported in cloud hosted windmill").into());
}
if send_email.email_recipients.is_none() {
use windmill_common::utils::report_critical_error;
tracing::error!("No recipient to send the error");
report_critical_error(
"No recipient to send the error".to_string(),
db.clone(),
Some(&w_id),
None,
)
.await;
return Err(anyhow::anyhow!("No recipient to send the error").into());
}
if authed.email == EMAIL_ERROR_HANDLER_USER_EMAIL
|| is_super_admin_email(&db, &authed.email).await?
{
let resp = send_workspace_trigger_failure_email_notification(
&db,
&w_id,
&send_email.job_id,
send_email.trigger_path.as_deref(),
send_email.runnable_path.as_deref(),
&send_email.email_recipients.unwrap(),
&send_email.error,
)
.await?;
return Ok(Json(resp));
}
return Err(Error::NotAuthorized(
"Only super admin or whitelisted token can access email workspace error handler feature"
.to_string(),
));
}
#[cfg(not(all(feature = "enterprise", feature = "smtp")))]
async fn send_email_with_instance_smtp(
_authed: ApiAuthed,
Extension(_db): Extension<DB>,
Path(_w_id): Path<String>,
Json(_send_email): Json<SendEmail>,
) -> error::Result<Json<String>> {
tracing::warn!("SMTP is not enabled, skipping workspace trigger failure email notification",);
return Err(anyhow::anyhow!("SMTP is not enabled").into());
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
async fn get_logs_from_store(
log_offset: i32,
@@ -1116,11 +1362,55 @@ async fn get_logs_from_disk(
return None;
}
async fn get_completed_job_logs_tail(
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::JsonResult<String> {
let tags = opt_authed
.as_ref()
.map(|authed| get_scope_tags(authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec()))
.flatten();
let record = sqlx::query!(
"SELECT created_by AS \"created_by!\", coalesce(job_logs.logs, '') as logs
FROM v2_job
LEFT JOIN job_logs ON job_logs.job_id = v2_job.id
WHERE v2_job.id = $1 AND v2_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_job.tag = ANY($3))
ORDER BY job_logs.log_offset DESC
LIMIT 100",
id,
w_id,
tags.as_ref().map(|v| v.as_slice())
)
.fetch_optional(&db)
.await?;
if let Some(record) = record {
if opt_authed.is_none() && record.created_by != "anonymous" {
return Err(Error::BadRequest(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
let logs = record.logs.unwrap_or_default();
Ok(Json(logs))
} else {
Err(Error::NotFound("Job not found".to_string()).into())
}
}
#[derive(Debug, Deserialize)]
struct QueryJobLogs {
remove_ansi_warnings: Option<bool>,
}
async fn get_job_logs(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(query_job_logs): Query<QueryJobLogs>,
) -> error::Result<Response> {
// let audit_author: AuditAuthor = match opt_authed {
// Some(authed) => (&authed).into(),
@@ -1174,10 +1464,14 @@ async fn get_job_logs(
{
return r.map(content_plain);
}
let logs = format!(
"to remove ansi colors, use: | sed 's/\\x1B\\[[0-9;]\\{{1,\\}}[A-Za-z]//g'\n{}",
let logs = if query_job_logs.remove_ansi_warnings.unwrap_or(false) {
logs
);
} else {
format!(
"to remove ansi colors, use: | sed 's/\\x1B\\[[0-9;]\\{{1,\\}}[A-Za-z]//g'\n{}",
logs
)
};
Ok(content_plain(Body::from(logs)))
} else {
let text = sqlx::query!(
@@ -6151,13 +6445,17 @@ async fn get_job_update_data(
if running.is_some_and(|x| !x) {
let r = sqlx::query!(
"SELECT
result as \"result: sqlx::types::Json<Box<RawValue>>\",
v2_job_queue.running as \"running: Option<bool>\",
COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",
jq.running as \"running: Option<bool>\",
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
CHAR_LENGTH(rs.stream) + 1 AS stream_offset
FROM v2_job_completed FULL OUTER JOIN v2_job_queue USING (id)
LEFT JOIN job_result_stream rs ON rs.job_id = $1
WHERE (v2_job_queue.id = $1 AND v2_job_queue.workspace_id = $2) OR (v2_job_completed.id = $1 AND v2_job_completed.workspace_id = $2)",
FROM (
SELECT $1::uuid as job_id, $2::text as workspace_id
) base
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
WHERE base.job_id = $1",
job_id,
w_id,
stream_offset.unwrap_or(0),
@@ -6170,15 +6468,22 @@ async fn get_job_update_data(
}
} else {
let q = sqlx::query!(
"SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", CHAR_LENGTH(rs.stream) + 1 AS stream_offset
FROM v2_job_completed FULL OUTER JOIN job_result_stream rs ON rs.job_id = v2_job_completed.id WHERE (v2_job_completed.id = $2 AND v2_job_completed.workspace_id = $1 OR rs.workspace_id = $1)",
"SELECT
COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\",
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
CHAR_LENGTH(rs.stream) + 1 AS stream_offset
FROM (
SELECT $2::uuid as job_id, $1::text as workspace_id
) base
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
WHERE base.job_id = $2",
w_id,
job_id,
stream_offset.unwrap_or(0),
)
.fetch_optional(db)
.await?;
tracing::error!("q {:?}", q);
if let Some(r) = q {
(r.result.map(|x| x.0), running, r.result_stream.flatten(), r.stream_offset)
} else {
+14 -1
View File
@@ -543,6 +543,18 @@ pub async fn run_server(
(Router::new(), Option::<()>::None)
};
let mcp_list_tools_service = {
#[cfg(feature = "mcp")]
{
mcp::list_tools_service()
}
#[cfg(not(feature = "mcp"))]
{
Router::new()
}
};
#[cfg(feature = "agent_worker_server")]
let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) =
if server_mode {
@@ -631,6 +643,8 @@ pub async fn run_server(
.nest("/embeddings", embeddings::global_service())
.nest("/ai", ai::global_service())
.nest("/inkeep", inkeep_oss::global_service())
.nest("/mcp/w/:workspace_id/sse", mcp_router)
.nest("/mcp/w/:workspace_id/list_tools", mcp_list_tools_service)
.route_layer(from_extractor::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/jobs", jobs::global_root_service())
@@ -669,7 +683,6 @@ pub async fn run_server(
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest("/mcp/w/:workspace_id/sse", mcp_router.layer(from_extractor::<ApiAuthed>()))
.layer(from_extractor::<OptAuthed>())
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
//! Model Context Protocol (MCP) implementation for Windmill
//!
//! This module provides the MCP server implementation that exposes Windmill scripts,
//! flows, and API endpoints as MCP tools for AI assistants to interact with.
pub mod server;
pub mod tools;
pub mod utils;
// Re-export main components
pub use server::{extract_and_store_workspace_id, setup_mcp_server, shutdown_mcp_server, list_tools_service};
+517
View File
@@ -0,0 +1,517 @@
//! MCP Server implementation
//!
//! Contains the core MCP server handler that implements the Model Context Protocol
//! specification. This is a thin orchestration layer that delegates to the appropriate
//! modules for tool management, database operations, and schema transformation.
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use axum::body::to_bytes;
use rmcp::{
handler::server::ServerHandler,
model::*,
service::{RequestContext, RoleServer},
Error,
};
use serde_json::Value;
use tokio::try_join;
use windmill_common::db::UserDB;
use windmill_common::worker::to_raw_value;
use windmill_common::{utils::StripPath, DB};
use crate::db::ApiAuthed;
use crate::jobs::{
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
};
use super::utils::{
database::{
check_scopes, get_items, get_resources_types, get_scripts_from_hub, get_item_schema, get_hub_script_schema
},
models::{ScriptInfo, FlowInfo, ResourceInfo, ResourceType, SchemaType, ToolableItem, WorkspaceId},
schema::transform_schema_for_resources,
transform::{reverse_transform, reverse_transform_key},
};
use super::tools::{
endpoint_tools::{all_endpoint_tools, endpoint_tools_to_mcp_tools, call_endpoint_tool, EndpointTool},
};
use axum::{
extract::Path,
http::Request,
middleware::Next,
response::Response,
routing::get,
Json,
Router,
};
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager,
SessionManager,
StreamableHttpService,
};
use windmill_common::error::JsonResult;
/// MCP Server Runner - implements the core MCP protocol handlers
#[derive(Clone)]
pub struct Runner {}
impl Runner {
pub fn new() -> Self {
Self {}
}
/// Creates a Tool from a ToolableItem
async fn create_tool_from_item<T: ToolableItem>(
item: &T,
user_db: &UserDB,
authed: &ApiAuthed,
workspace_id: &str,
resources_cache: &mut HashMap<String, Vec<ResourceInfo>>,
resources_types: &Vec<ResourceType>,
) -> Result<Tool, Error> {
let is_hub = item.is_hub();
let path = item.get_path_or_id();
let item_type = item.item_type();
let description = format!(
"This is a {} named `{}` with the following description: `{}`.{}",
item_type,
item.get_summary(),
item.get_description(),
if is_hub {
format!(
" It is a tool used for the following app: {}",
item.get_integration_type()
.unwrap_or("No integration type".to_string())
)
} else {
"".to_string()
}
);
let schema_obj = transform_schema_for_resources(
&item.get_schema(),
user_db,
authed,
&workspace_id,
resources_cache,
&resources_types,
)
.await?;
let input_schema_map = match serde_json::to_value(schema_obj) {
Ok(Value::Object(map)) => map,
Ok(_) => {
tracing::warn!("Schema object for tool '{}' did not serialize to a JSON object, using empty schema.", path);
serde_json::Map::new()
}
Err(e) => {
tracing::error!(
"Failed to serialize schema object for tool '{}': {}. Using empty schema.",
path,
e
);
serde_json::Map::new()
}
};
Ok(Tool {
name: Cow::Owned(path),
description: Some(Cow::Owned(description)),
input_schema: Arc::new(input_schema_map),
annotations: Some(ToolAnnotations {
title: Some(item.get_summary().to_string()),
read_only_hint: Some(false), // Can modify environment
destructive_hint: Some(true), // Can potentially be destructive
idempotent_hint: Some(false), // Are not guaranteed to be idempotent
open_world_hint: Some(true), // Can interact with external services
}),
})
}
}
impl ServerHandler for Runner {
/// Handles the `CallTool` request from the MCP client
async fn call_tool(
&self,
request: CallToolRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, Error> {
let http_parts = context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
check_scopes(authed)?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let args = request.arguments.map(Value::Object).ok_or_else(|| {
Error::invalid_params("Missing arguments for tool", Some(request.name.clone().into()))
})?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
// Check if this is a generated endpoint tool
let endpoint_tools = all_endpoint_tools();
for endpoint_tool in endpoint_tools {
if endpoint_tool.name.as_ref() == request.name {
// This is an endpoint tool, forward to the actual HTTP endpoint
let result = call_endpoint_tool(&endpoint_tool, args.clone(), &workspace_id, &authed).await?;
return Ok(CallToolResult::success(vec![Content::text(
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string())
)]));
}
}
// Continue with script/flow logic
let (tool_type, path, is_hub) =
reverse_transform(&request.name).unwrap_or_default();
let item_schema = if is_hub {
get_hub_script_schema(&format!("hub/{}", path), db).await?
} else {
get_item_schema(&path, user_db, authed, &workspace_id, &tool_type).await?
};
let schema_obj = if let Some(ref s) = item_schema {
match serde_json::from_str::<SchemaType>(s.0.get()) {
Ok(val) => Some(val),
Err(e) => {
tracing::warn!("Failed to parse schema: {}", e);
None
}
}
} else {
None
};
let push_args = if let Value::Object(map) = args.clone() {
let mut args_hash = HashMap::new();
for (k, v) in map {
// need to transform back the key without invalid characters to the original key
let original_key = reverse_transform_key(&k, &schema_obj);
args_hash.insert(original_key, to_raw_value(&v));
}
windmill_queue::PushArgsOwned { extra: None, args: args_hash }
} else {
windmill_queue::PushArgsOwned::default()
};
let script_or_flow_path = if is_hub {
StripPath(format!("hub/{}", path))
} else {
StripPath(path)
};
let run_query = RunJobQuery::default();
let result = if tool_type == "script" {
run_wait_result_script_by_path_internal(
db.clone(),
run_query,
script_or_flow_path,
authed.clone(),
user_db.clone(),
workspace_id.clone(),
push_args,
)
.await
} else {
run_wait_result_flow_by_path_internal(
db.clone(),
run_query,
script_or_flow_path,
authed.clone(),
user_db.clone(),
push_args,
workspace_id.clone(),
)
.await
};
match result {
Ok(response) => {
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.map_err(|e| {
Error::internal_error(format!("Failed to read response body: {}", e), None)
})?;
let body_str = String::from_utf8(body_bytes.to_vec()).map_err(|e| {
Error::internal_error(format!("Failed to decode response body: {}", e), None)
})?;
Ok(CallToolResult::success(vec![Content::text(body_str)]))
}
Err(e) => Err(Error::internal_error(
format!("Failed to run script: {}", e),
None,
)),
}
}
/// Fetches available tools (scripts, flows, hub scripts) based on the user's scope
async fn list_tools(
&self,
_request: Option<PaginatedRequestParam>,
mut _context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, Error> {
let http_parts = _context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
check_scopes(authed)?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
let scopes = authed.scopes.as_ref();
let owned_scope = scopes.and_then(|scopes| {
scopes
.iter()
.find(|scope| scope.starts_with("mcp:") && !scope.contains("hub"))
});
let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
let (scope_type, scope_path) = owned_scope.map_or(("all", None), |scope| {
let parts = scope.split(":").collect::<Vec<&str>>();
(parts[1], if parts.len() == 3 { Some(parts[2]) } else { None })
});
let scope_integrations = hub_scope.and_then(|scope| {
let parts = scope.split(":").collect::<Vec<&str>>();
if parts.len() == 3 {
Some(parts[2])
} else {
None
}
});
let scripts_fn = get_items::<ScriptInfo>(
user_db,
authed,
&workspace_id,
scope_type,
"script",
scope_path.as_deref(),
);
let flows_fn =
get_items::<FlowInfo>(user_db, authed, &workspace_id, scope_type, "flow", scope_path.as_deref());
let resources_types_fn = get_resources_types(user_db, authed, &workspace_id);
let hub_scripts_fn = get_scripts_from_hub(db, scope_integrations.as_deref());
let (scripts, flows, resources_types, hub_scripts) = if scope_integrations.is_some() {
let (scripts, flows, resources_types, hub_scripts) =
try_join!(scripts_fn, flows_fn, resources_types_fn, hub_scripts_fn)?;
(scripts, flows, resources_types, hub_scripts)
} else {
let (scripts, flows, resources_types) =
try_join!(scripts_fn, flows_fn, resources_types_fn)?;
(scripts, flows, resources_types, vec![])
};
let mut resources_cache: HashMap<String, Vec<ResourceInfo>> = HashMap::new();
let mut tools: Vec<Tool> = Vec::new();
for script in scripts {
tools.push(
Runner::create_tool_from_item(
&script,
user_db,
authed,
&workspace_id,
&mut resources_cache,
&resources_types,
)
.await?,
);
}
for flow in flows {
tools.push(
Runner::create_tool_from_item(
&flow,
user_db,
authed,
&workspace_id,
&mut resources_cache,
&resources_types,
)
.await?,
);
}
for hub_script in hub_scripts {
tools.push(
Runner::create_tool_from_item(
&hub_script,
user_db,
authed,
&workspace_id,
&mut resources_cache,
&resources_types,
)
.await?,
);
}
// Add endpoint tools from the generated MCP tools
let endpoint_tools = all_endpoint_tools();
let mcp_tools_converted = endpoint_tools_to_mcp_tools(endpoint_tools);
tools.extend(mcp_tools_converted);
Ok(ListToolsResult { tools, next_cursor: None })
}
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: Default::default(),
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_tool_list_changed()
.build(),
server_info: Implementation::from_build_env(),
instructions: Some("This server provides a list of scripts and flows the user can run on Windmill. Each flow and script is a tool callable with their respective arguments.".to_string()),
}
}
async fn initialize(
&self,
_request: InitializeRequestParam,
_context: RequestContext<RoleServer>,
) -> Result<InitializeResult, Error> {
Ok(self.get_info())
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, Error> {
Ok(ListResourcesResult { resources: vec![], next_cursor: None })
}
async fn list_prompts(
&self,
_request: Option<PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, Error> {
Ok(ListPromptsResult::default())
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, Error> {
Ok(ListResourceTemplatesResult::default())
}
}
/// Extract workspace ID from path and store it in request extensions
pub async fn extract_and_store_workspace_id(
Path(params): Path<String>,
mut request: Request<axum::body::Body>,
next: Next,
) -> Response {
let workspace_id = params;
request.extensions_mut().insert(WorkspaceId(workspace_id));
next.run(request).await
}
/// Setup the MCP server with HTTP transport
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
let session_manager = Arc::new(LocalSessionManager::default());
let service_config = Default::default();
let service = StreamableHttpService::new(
|| Ok(Runner::new()),
session_manager.clone(),
service_config,
);
let router = axum::Router::new().nest_service("/", service);
Ok((router, session_manager))
}
/// Shutdown the MCP server gracefully by closing all active sessions
pub async fn shutdown_mcp_server(session_manager: Arc<LocalSessionManager>) {
let session_ids_to_close = {
let sessions_map = session_manager.sessions.read().await;
sessions_map.keys().cloned().collect::<Vec<_>>()
};
if !session_ids_to_close.is_empty() {
tracing::info!(
"Closing {} active MCP session(s)...",
session_ids_to_close.len()
);
let close_futures = session_ids_to_close
.iter()
.map(|session_id| {
let manager_clone = session_manager.clone();
async move {
if let Err(_) = manager_clone.close_session(session_id).await {
tracing::warn!("Error closing MCP session");
}
}
})
.collect::<Vec<_>>();
futures::future::join_all(close_futures).await;
}
}
/// HTTP handler to list MCP tools as JSON
async fn list_mcp_tools_handler() -> JsonResult<Vec<EndpointTool>> {
let endpoint_tools = all_endpoint_tools();
Ok(Json(endpoint_tools))
}
/// Creates a router service for listing MCP tools
pub fn list_tools_service() -> Router {
Router::new()
.route("/", get(list_mcp_tools_handler))
}

Some files were not shown because too many files have changed in this diff Show More