From 2a0c4cd1c9a0ea641a53fcf53499f5bb0925800d Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:28:56 +0100 Subject: [PATCH] feat(mcp): add documentation endpoint (#7331) * simplify inkeep endpoint + add to openapi * adapt frontend * add to mcp endpoints * nit * udpate ref --- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 49 +++++++-- backend/windmill-api/src/inkeep_oss.rs | 16 ++- .../src/mcp/tools/auto_generated_endpoints.rs | 102 +++++++++++++++--- .../components/copilot/chat/navigator/core.ts | 34 ++++-- frontend/src/lib/mcpEndpointTools.ts | 102 +++++++++++++++--- 6 files changed, 252 insertions(+), 53 deletions(-) diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 62a5c1e615..7794045f31 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -8f6e0bd9f273ac9da5f4fb3a44979f17b8e1b9b5 \ No newline at end of file +ffc3e065de448f94d2cd6b39d4aac917e1a77307 \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6a5c91b598..ea64eac054 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -69,6 +69,41 @@ paths: schema: type: string + /inkeep: + post: + summary: query Windmill AI documentation assistant (EE only) + operationId: queryDocumentation + x-mcp-tool: true + tags: + - documentation + requestBody: + description: query to send to the AI documentation assistant + required: true + content: + application/json: + schema: + type: object + properties: + query: + type: string + description: The documentation query to send to the AI assistant + required: + - query + responses: + "200": + description: AI documentation assistant response + content: + application/json: + schema: + type: object + description: Response from Inkeep service + "403": + description: Enterprise Edition required + content: + text/plain: + schema: + type: string + /openapi.yaml: get: summary: get openapi yaml spec @@ -8930,9 +8965,9 @@ paths: "200": description: confirmation message content: - application/json: - schema: - type: string + application/json: + schema: + type: string /w/{workspace}/trigger/{trigger_kind}/cancel_suspended_trigger_jobs/{trigger_path}: post: @@ -16643,7 +16678,7 @@ components: "aiagent", "unassigned_script", "unassigned_flow", - "unassigned_singlestepflow" + "unassigned_singlestepflow", ] schedule_path: type: string @@ -16759,7 +16794,7 @@ components: "aiagent", "unassigned_script", "unassigned_flow", - "unassigned_singlestepflow" + "unassigned_singlestepflow", ] schedule_path: type: string @@ -16883,7 +16918,7 @@ components: "aiagent", "unassigned_script", "unassigned_flow", - "unassigned_singlestepflow" + "unassigned_singlestepflow", ] trigger: type: string @@ -17031,7 +17066,7 @@ components: "aiagent", "unassigned_script", "unassigned_flow", - "unassigned_singlestepflow" + "unassigned_singlestepflow", ] trigger: type: string diff --git a/backend/windmill-api/src/inkeep_oss.rs b/backend/windmill-api/src/inkeep_oss.rs index ca1ffebd21..a34ea91435 100644 --- a/backend/windmill-api/src/inkeep_oss.rs +++ b/backend/windmill-api/src/inkeep_oss.rs @@ -3,9 +3,21 @@ pub use crate::inkeep_ee::*; #[cfg(not(feature = "private"))] -use axum::Router; +use axum::{routing::post, Router}; + +#[cfg(not(feature = "private"))] +use windmill_common::error::Error; #[cfg(not(feature = "private"))] pub fn global_service() -> Router { - Router::new() + Router::new().route("/", post(inkeep_not_available)) +} + +#[cfg(not(feature = "private"))] +async fn inkeep_not_available() -> windmill_common::error::Result<()> { + Err(Error::Generic( + http::StatusCode::FORBIDDEN, + "Inkeep AI documentation assistant is only available in Windmill Enterprise Edition" + .to_string(), + )) } diff --git a/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs index 629d4d6b8c..a33b80f9ba 100644 --- a/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs @@ -20,6 +20,27 @@ pub struct EndpointTool { pub fn all_tools() -> Vec { vec![ + EndpointTool { + name: Cow::Borrowed("queryDocumentation"), + description: Cow::Borrowed("query Windmill AI documentation assistant (EE only)"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/inkeep"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The documentation query to send to the AI assistant" + } + }, + "required": [ + "query" + ] +})), + }, EndpointTool { name: Cow::Borrowed("createVariable"), description: Cow::Borrowed("create variable"), @@ -447,6 +468,10 @@ pub fn all_tools() -> Vec { "languages": { "type": "string", "description": "Filter to only include scripts written in the given languages.\nAccepts multiple values as a comma-separated list.\n" + }, + "without_description": { + "type": "boolean", + "description": "(default false)\nIf true, the description field will be omitted from the response.\n" } }, "required": [] @@ -530,6 +555,10 @@ pub fn all_tools() -> Vec { "with_deployment_msg": { "type": "boolean", "description": "(default false)\ninclude deployment message\n" + }, + "without_description": { + "type": "boolean", + "description": "(default false)\nIf true, the description field will be omitted from the response.\n" } }, "required": [] @@ -683,6 +712,28 @@ pub fn all_tools() -> Vec { "type": "string", "description": "mask to filter by schedule path" }, + "trigger_path": { + "type": "string", + "description": "mask to filter by trigger path" + }, + "trigger_kind": { + "description": "trigger kind (schedule, http, websocket...)", + "type": "string", + "enum": [ + "webhook", + "default_email", + "email", + "schedule", + "http", + "websocket", + "postgres", + "kafka", + "nats", + "mqtt", + "sqs", + "gcp" + ] + }, "script_hash": { "type": "string", "description": "mask to filter exact matching path" @@ -817,10 +868,25 @@ pub fn all_tools() -> Vec { "format": "date-time", "description": "filter on created after (exclusive) timestamp" }, - "created_or_started_before": { + "completed_before": { "type": "string", "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise before (inclusive) timestamp" + "description": "filter on started before (inclusive) timestamp" + }, + "completed_after": { + "type": "string", + "format": "date-time", + "description": "filter on started after (exclusive) timestamp" + }, + "created_before_queue": { + "type": "string", + "format": "date-time", + "description": "filter on jobs created before X for jobs in the queue only" + }, + "created_after_queue": { + "type": "string", + "format": "date-time", + "description": "filter on jobs created after X for jobs in the queue only" }, "running": { "type": "boolean", @@ -830,16 +896,6 @@ pub fn all_tools() -> Vec { "type": "boolean", "description": "filter on jobs scheduled_for before now (hence waitinf for a worker)" }, - "created_or_started_after": { - "type": "string", - "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise after (exclusive) timestamp" - }, - "created_or_started_after_completed_jobs": { - "type": "string", - "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise after (exclusive) timestamp but only for the completed jobs" - }, "job_kinds": { "type": "string", "description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by," @@ -864,14 +920,28 @@ pub fn all_tools() -> Vec { "type": "boolean", "description": "allow wildcards (*) in the filter of label, tag, worker" }, - "page": { - "type": "integer", - "description": "which page to return (start at 1, default 1)" - }, "per_page": { "type": "integer", "description": "number of items to return for a given page (default 30, max 100)" }, + "trigger_kind": { + "description": "trigger kind (schedule, http, websocket...)", + "type": "string", + "enum": [ + "webhook", + "default_email", + "email", + "schedule", + "http", + "websocket", + "postgres", + "kafka", + "nats", + "mqtt", + "sqs", + "gcp" + ] + }, "is_skipped": { "type": "boolean", "description": "is the job skipped" diff --git a/frontend/src/lib/components/copilot/chat/navigator/core.ts b/frontend/src/lib/components/copilot/chat/navigator/core.ts index 4c9cf38b70..d43de36f5e 100644 --- a/frontend/src/lib/components/copilot/chat/navigator/core.ts +++ b/frontend/src/lib/components/copilot/chat/navigator/core.ts @@ -235,13 +235,15 @@ async function getDocumentation(args: { request: string }): Promise { 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: 'inkeep-rag', - messages: [{ role: 'user', content: args.request }], - response_format: { - type: 'json_object' - } + query: args.request }) }) + + if (!retrieval.ok) { + const errorText = await retrieval.text() + throw new Error(errorText) + } + const data = await retrieval.json() if (!data.choices?.[0]?.message?.content) { return 'No documentation found for this request' @@ -291,11 +293,11 @@ const getTriggerableComponentsTool: Tool<{}> = { def: GET_TRIGGERABLE_COMPONENTS_TOOL, fn: async ({ toolId, toolCallbacks }) => { toolCallbacks.setToolStatus(toolId, { - content: 'Scanning the page...', + content: 'Scanning the page...' }) const components = getTriggerableComponents() toolCallbacks.setToolStatus(toolId, { - content: 'Scanned the page', + content: 'Scanned the page' }) return components } @@ -319,9 +321,14 @@ export const getDocumentationTool: Tool<{}> = { toolCallbacks.setToolStatus(toolId, { content: 'Retrieved documentation' }) return docResult } catch (error) { - toolCallbacks.setToolStatus(toolId, { content: 'Error getting documentation', error: 'Error getting documentation' }) + toolCallbacks.setToolStatus(toolId, { + content: 'Error getting documentation', + error: 'Error getting documentation' + }) console.error('Error getting documentation:', error) - return 'Failed to get documentation, pursuing with the user request...' + const errorMessage = + error instanceof Error ? error.message : 'An error occurred while getting documentation' + return `Failed to get documentation: ${errorMessage}, pursuing with the user request...` } } } @@ -335,7 +342,10 @@ const getAvailableResourcesTool: Tool<{}> = { toolCallbacks.setToolStatus(toolId, { content: 'Retrieved available resources' }) return resources } catch (error) { - toolCallbacks.setToolStatus(toolId, { content: 'Error getting available resources', error: 'Error getting available resources' }) + toolCallbacks.setToolStatus(toolId, { + content: 'Error getting available resources', + error: 'Error getting available resources' + }) console.error('Error getting available resources:', error) return 'Failed to get available resources, pursuing with the user request...' } @@ -350,7 +360,9 @@ export const navigatorTools: Tool<{}>[] = [ getAvailableResourcesTool ] -export function prepareNavigatorSystemMessage(customPrompt?: string): ChatCompletionSystemMessageParam { +export function prepareNavigatorSystemMessage( + customPrompt?: string +): ChatCompletionSystemMessageParam { let content = CHAT_SYSTEM_PROMPT // If there's a custom prompt, append it to the system prompt diff --git a/frontend/src/lib/mcpEndpointTools.ts b/frontend/src/lib/mcpEndpointTools.ts index 9b45f2bf41..8f825f79df 100644 --- a/frontend/src/lib/mcpEndpointTools.ts +++ b/frontend/src/lib/mcpEndpointTools.ts @@ -13,6 +13,27 @@ export interface EndpointTool { } export const mcpEndpointTools: EndpointTool[] = [ + { + name: "queryDocumentation", + description: "query Windmill AI documentation assistant (EE only)", + instructions: "", + path: "/inkeep", + method: "POST", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The documentation query to send to the AI assistant" + } + }, + "required": [ + "query" + ] +} + }, { name: "createVariable", description: "create variable", @@ -440,6 +461,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "languages": { "type": "string", "description": "Filter to only include scripts written in the given languages.\nAccepts multiple values as a comma-separated list.\n" + }, + "without_description": { + "type": "boolean", + "description": "(default false)\nIf true, the description field will be omitted from the response.\n" } }, "required": [] @@ -523,6 +548,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "with_deployment_msg": { "type": "boolean", "description": "(default false)\ninclude deployment message\n" + }, + "without_description": { + "type": "boolean", + "description": "(default false)\nIf true, the description field will be omitted from the response.\n" } }, "required": [] @@ -676,6 +705,28 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string", "description": "mask to filter by schedule path" }, + "trigger_path": { + "type": "string", + "description": "mask to filter by trigger path" + }, + "trigger_kind": { + "description": "trigger kind (schedule, http, websocket...)", + "type": "string", + "enum": [ + "webhook", + "default_email", + "email", + "schedule", + "http", + "websocket", + "postgres", + "kafka", + "nats", + "mqtt", + "sqs", + "gcp" + ] + }, "script_hash": { "type": "string", "description": "mask to filter exact matching path" @@ -810,10 +861,25 @@ export const mcpEndpointTools: EndpointTool[] = [ "format": "date-time", "description": "filter on created after (exclusive) timestamp" }, - "created_or_started_before": { + "completed_before": { "type": "string", "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise before (inclusive) timestamp" + "description": "filter on started before (inclusive) timestamp" + }, + "completed_after": { + "type": "string", + "format": "date-time", + "description": "filter on started after (exclusive) timestamp" + }, + "created_before_queue": { + "type": "string", + "format": "date-time", + "description": "filter on jobs created before X for jobs in the queue only" + }, + "created_after_queue": { + "type": "string", + "format": "date-time", + "description": "filter on jobs created after X for jobs in the queue only" }, "running": { "type": "boolean", @@ -823,16 +889,6 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "boolean", "description": "filter on jobs scheduled_for before now (hence waitinf for a worker)" }, - "created_or_started_after": { - "type": "string", - "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise after (exclusive) timestamp" - }, - "created_or_started_after_completed_jobs": { - "type": "string", - "format": "date-time", - "description": "filter on created_at for non non started job and started_at otherwise after (exclusive) timestamp but only for the completed jobs" - }, "job_kinds": { "type": "string", "description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by," @@ -857,14 +913,28 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "boolean", "description": "allow wildcards (*) in the filter of label, tag, worker" }, - "page": { - "type": "integer", - "description": "which page to return (start at 1, default 1)" - }, "per_page": { "type": "integer", "description": "number of items to return for a given page (default 30, max 100)" }, + "trigger_kind": { + "description": "trigger kind (schedule, http, websocket...)", + "type": "string", + "enum": [ + "webhook", + "default_email", + "email", + "schedule", + "http", + "websocket", + "postgres", + "kafka", + "nats", + "mqtt", + "sqs", + "gcp" + ] + }, "is_skipped": { "type": "boolean", "description": "is the job skipped"