From 3663bc6ed90dfa9851ded3056371a5462489b767 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Thu, 26 Feb 2026 13:50:07 +0100 Subject: [PATCH] =?UTF-8?q?Implement=20Proposal=2073:=20rename=20MCP=20too?= =?UTF-8?q?l=20prefix=20task=5F=20=E2=86=92=20plan=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All public MCP tool names are renamed: task_create → plan_create task_status → plan_status task_stop → plan_stop task_retry → plan_retry task_list → plan_list task_file_info → plan_file_info (cloud) task_download → plan_download (local) The task_id UUID field name is intentionally unchanged. Changes: - mcp_cloud/tool_models.py: rename 18 Python classes (Task* → Plan*) with backward-compat aliases - mcp_cloud/app.py: schema constants, ToolDefinition names, handler functions, TOOL_HANDLERS dict, server instructions - mcp_cloud/http_server.py: imports, FastMCP wrapper functions, handler_map keys, payload.tool check - mcp_local/planexe_mcp_local.py: schema constants, ToolDefinition names, handler functions, _call_remote_tool() calls, TOOL_HANDLERS, server instructions - Test files: updated imports and string literal assertions (5 files) - Docs: updated all tool name references (13 files) - Proposal 73 status: Draft → Implemented Co-Authored-By: Claude Sonnet 4.6 --- README.md | 10 +- docs/mcp/codex.md | 2 +- docs/mcp/inspector.md | 14 +- docs/mcp/mcp_details.md | 34 +-- docs/mcp/mcp_setup.md | 22 +- docs/mcp/mcp_welcome.md | 4 +- docs/mcp/planexe_mcp_interface.md | 88 ++++---- .../73-rename-task-prefix-in-mcp-tools.md | 2 +- mcp_cloud/AGENTS.md | 28 +-- mcp_cloud/README.md | 38 ++-- mcp_cloud/app.py | 208 ++++++++++-------- mcp_cloud/http_server.py | 72 +++--- mcp_cloud/tests/test_cors_config.py | 8 +- mcp_cloud/tests/test_task_create_tool.py | 4 +- mcp_cloud/tests/test_task_file_info_tool.py | 4 +- mcp_cloud/tests/test_task_retry_tool.py | 4 +- mcp_cloud/tests/test_task_status_tool.py | 2 +- .../tests/test_tool_surface_consistency.py | 130 +++++------ mcp_cloud/tool_models.py | 93 +++++--- mcp_local/AGENTS.md | 26 +-- mcp_local/README.md | 32 +-- mcp_local/planexe_mcp_local.py | 176 ++++++++------- public/llms.txt | 52 ++--- skills/planexe-mcp/SKILL.md | 36 +-- 24 files changed, 583 insertions(+), 506 deletions(-) diff --git a/README.md b/README.md index 0073c5c52..88cc93cb3 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ The Tool workflow (tools-only, not MCP tasks protocol) 1. `prompt_examples` 2. `model_profiles` (optional, helps choose `model_profile`) 3. non-tool step: draft/approve prompt -4. `task_create` -5. `task_status` (poll every 5 minutes until done) -6. optional if failed: `task_retry` -7. download the result via `task_download` or via `task_file_info` +4. `plan_create` +5. `plan_status` (poll every 5 minutes until done) +6. optional if failed: `plan_retry` +7. download the result via `plan_download` or via `plan_file_info` -Concurrency note: each `task_create` call returns a new `task_id`; server-side global per-client concurrency is not capped, so clients should track their own parallel tasks. +Concurrency note: each `plan_create` call returns a new `task_id`; server-side global per-client concurrency is not capped, so clients should track their own parallel tasks. ### Option A: Remote MCP (fastest path) diff --git a/docs/mcp/codex.md b/docs/mcp/codex.md index b0708ca71..92c1e6ac3 100644 --- a/docs/mcp/codex.md +++ b/docs/mcp/codex.md @@ -16,7 +16,7 @@ Guide for connecting [codex](https://openai.com/codex/) with PlanExe via MCP. 1. Start Codex. 2. Ask for MCP tools. 3. Call `prompt_examples` to get examples. -4. Call `task_create` to start a plan. +4. Call `plan_create` to start a plan. ## Sample prompt diff --git a/docs/mcp/inspector.md b/docs/mcp/inspector.md index 5e2117781..ef303fe5d 100644 --- a/docs/mcp/inspector.md +++ b/docs/mcp/inspector.md @@ -71,14 +71,14 @@ Now there should be a list with tool names and descriptions: ``` prompt_examples model_profiles -task_create -task_status -task_stop -task_retry -task_file_info +plan_create +plan_status +plan_stop +plan_retry +plan_file_info ``` -When you inspect `task_create`, the visible input schema includes `prompt` and optional `model_profile`. +When you inspect `plan_create`, the visible input schema includes `prompt` and optional `model_profile`. Follow these steps: ![screenshot of mcp inspector invoke tool](inspector_step5_mcp_planexe_org.webp) @@ -86,7 +86,7 @@ Follow these steps: 1. In the `Tools` panel; Click on the `prompt_examples` tool. 2. In the `prompt_examples` right sidepanel; Click on `Run Tool`. 3. The MCP server should respond with a list of example prompts. -4. Optionally run `model_profiles` to inspect available `model_profile` choices before `task_create`. +4. Optionally run `model_profiles` to inspect available `model_profile` choices before `plan_create`. ## Approach 2. MCP server inside docker diff --git a/docs/mcp/mcp_details.md b/docs/mcp/mcp_details.md index f485ee72b..8b9ea5e87 100644 --- a/docs/mcp/mcp_details.md +++ b/docs/mcp/mcp_details.md @@ -10,13 +10,13 @@ This document lists the MCP tools exposed by PlanExe and example prompts for age - The primary MCP server runs in the cloud (see `mcp_cloud`). - The local MCP proxy (`mcp_local`) forwards calls to the server and adds a local download helper. - Tool responses return JSON in both `content.text` and `structuredContent`. -- Workflow note: drafting and user approval of the prompt is a non-tool step between setup tools and `task_create`. +- Workflow note: drafting and user approval of the prompt is a non-tool step between setup tools and `plan_create`. ## Tool Catalog, `mcp_cloud` ### prompt_examples -Returns around five example prompts that show what good prompts look like. Each sample is typically 300-800 words. Usually the AI does the heavy lifting: the user has a vague idea, the agent calls `prompt_examples`, then expands that idea into a high-quality prompt (300-800 words). A compact prompt shape works best: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. The prompt is shown to the user, who can ask for further changes or confirm it’s good to go. When the user confirms, the agent then calls `task_create`. Shorter or vaguer prompts produce lower-quality plans. +Returns around five example prompts that show what good prompts look like. Each sample is typically 300-800 words. Usually the AI does the heavy lifting: the user has a vague idea, the agent calls `prompt_examples`, then expands that idea into a high-quality prompt (300-800 words). A compact prompt shape works best: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. The prompt is shown to the user, who can ask for further changes or confirm it’s good to go. When the user confirms, the agent then calls `plan_create`. Shorter or vaguer prompts produce lower-quality plans. Example prompt: ``` @@ -32,7 +32,7 @@ Response includes `samples` (array of prompt strings, each ~300-800 words) and ` ### model_profiles -Returns profile guidance and model availability for `task_create.model_profile`. +Returns profile guidance and model availability for `plan_create.model_profile`. This helps agents pick a profile without knowing internal `llm_config/*.json` details. Profiles with zero models are omitted from the `profiles` list. If no models are available in any profile, `model_profiles` returns `isError=true` with `error.code = MODEL_PROFILES_UNAVAILABLE`. @@ -56,7 +56,7 @@ Response includes: - `model_count` - `models[]` (`key`, `provider_class`, `model`, `priority`) -### task_create +### plan_create Create a new plan task. @@ -92,7 +92,7 @@ What to do instead: - For PlanExe, send a substantial multi-phase project prompt with scope, constraints, timeline, budget, stakeholders, and success criteria. - PlanExe always runs a fixed end-to-end pipeline; it does not support selecting only internal pipeline subsets. -### task_status +### plan_status Fetch status/progress and recent files for a task. @@ -113,7 +113,7 @@ State contract: - `completed`: terminal success, proceed to download. - `failed`: terminal error. -### task_stop +### plan_stop Request an active task to stop. @@ -127,7 +127,7 @@ Example call: {"task_id": "2d57a448-1b09-45aa-ad37-e69891ff6ec7"} ``` -### task_retry +### plan_retry Retry a failed task by requeueing the same `task_id`. @@ -146,7 +146,7 @@ Notes: - Only failed tasks can be retried. - Non-failed tasks return `TASK_NOT_FAILED`. -### task_file_info +### plan_file_info Return download metadata for report or zip artifacts. @@ -177,7 +177,7 @@ Typical successful response: ### Download with `curl` -When `task_file_info` returns a `download_url`, you can download directly with the same `X-API-Key` used for MCP authentication. +When `plan_file_info` returns a `download_url`, you can download directly with the same `X-API-Key` used for MCP authentication. Download zip: ```bash @@ -193,7 +193,7 @@ curl -H "X-API-Key: pex_0123456789abcdef" -O "https://mcp.planexe.org/download/2 The local proxy exposes the same tools as the server, and adds: -### task_download +### plan_download Download report or zip to a local path. @@ -207,7 +207,7 @@ Example call: {"task_id": "2d57a448-1b09-45aa-ad37-e69891ff6ec7", "artifact": "report"} ``` -`PLANEXE_PATH` behavior for `task_download`: +`PLANEXE_PATH` behavior for `plan_download`: - Save directory is `PLANEXE_PATH`, or current working directory if unset. - Non-existing directories are created automatically. - If `PLANEXE_PATH` points to a file, download fails. @@ -237,11 +237,11 @@ Common local proxy error codes: - `DOWNLOAD_FAILED` Special case: -- `task_file_info` may return `{}` while the artifact is not ready yet (not an error). +- `plan_file_info` may return `{}` while the artifact is not ready yet (not an error). ## Concurrency semantics (practical) -- Each `task_create` call creates a new task with a new `task_id`. +- Each `plan_create` call creates a new task with a new `task_id`. - The server does not enforce a global “one active task per client” cap. - Parallelism is a client orchestration concern: - start with 1 task @@ -282,7 +282,7 @@ At this step, the agent writes a high-quality prompt draft (typically 300-800 wo ### 4. Create a plan -The user reviews the prompt and either asks for further changes or confirms it’s good to go. When the user confirms, the agent calls `task_create` with that prompt. +The user reviews the prompt and either asks for further changes or confirms it’s good to go. When the user confirms, the agent calls `plan_create` with that prompt. Tool call: ```json @@ -298,14 +298,14 @@ Get status for my latest task. Tool call: ```json -{"task_id": ""} +{"task_id": ""} ``` If state is `failed`, optional retry: Tool call: ```json -{"task_id": "", "model_profile": "baseline"} +{"task_id": "", "model_profile": "baseline"} ``` ### 6. Download the report @@ -317,5 +317,5 @@ Download the report for my task. Tool call: ```json -{"task_id": "", "artifact": "report"} +{"task_id": "", "artifact": "report"} ``` diff --git a/docs/mcp/mcp_setup.md b/docs/mcp/mcp_setup.md index 772dca9e9..0d8594839 100644 --- a/docs/mcp/mcp_setup.md +++ b/docs/mcp/mcp_setup.md @@ -16,8 +16,8 @@ This is the shortest path to a working PlanExe MCP integration. Use this compact shape: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. 4. Create the plan task. 5. Poll for status (about every 5 minutes). -6. If status is `failed`, optionally call `task_retry` (defaults to `model_profile=baseline`). -7. Download artifacts via `task_file_info` (cloud) or `task_download` (mcp_local helper). +6. If status is `failed`, optionally call `plan_retry` (defaults to `model_profile=baseline`). +7. Download artifacts via `plan_file_info` (cloud) or `plan_download` (mcp_local helper). --- @@ -25,18 +25,18 @@ This is the shortest path to a working PlanExe MCP integration. 1. `prompt_examples` 2. `model_profiles` -3. `task_create` -4. `task_status` -5. `task_retry` (optional, only for failed tasks) -6. `task_file_info` +3. `plan_create` +4. `plan_status` +5. `plan_retry` (optional, only for failed tasks) +6. `plan_file_info` Optional local helper: -- `task_download` (provided by `mcp_local`, not `mcp_cloud`) +- `plan_download` (provided by `mcp_local`, not `mcp_cloud`) -For `task_create`: +For `plan_create`: - Visible arguments: `prompt` (required), `model_profile` (optional). -- Reference: [PlanExe MCP interface](planexe_mcp_interface.md#62-task_create) +- Reference: [PlanExe MCP interface](planexe_mcp_interface.md#62-plan_create) --- @@ -44,8 +44,8 @@ For `task_create`: - You can fetch example prompts. - You can create a plan task. -- You can fetch artifact metadata/URLs with `task_file_info` (and optionally save locally via `task_download` when using `mcp_local`). -- Your client can parse `error.code` and `error.message` and handle `{}` from `task_file_info` as "not ready yet". +- You can fetch artifact metadata/URLs with `plan_file_info` (and optionally save locally via `plan_download` when using `mcp_local`). +- Your client can parse `error.code` and `error.message` and handle `{}` from `plan_file_info` as "not ready yet". - If running parallel work, your client tracks multiple `task_id`s explicitly (server-side global cap is not enforced). --- diff --git a/docs/mcp/mcp_welcome.md b/docs/mcp/mcp_welcome.md index 876ce68f5..de1184437 100644 --- a/docs/mcp/mcp_welcome.md +++ b/docs/mcp/mcp_welcome.md @@ -20,9 +20,9 @@ No MCP experience is required to get started. ## What you can do - **Get example prompts** — See what good prompts look like (detailed, typically ~300-800 words). It is the **caller’s responsibility** to take inspiration from these examples and ensure the prompt sent to PlanExe is of similar or better quality. A compact prompt shape works best: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. The agent can refine a vague idea into a high-quality prompt and show it to the user for approval before creating the plan. -- **Create a plan** — Send a prompt; PlanExe starts creating the plan (typically takes 10–20 minutes on baseline profile). If the input prompt is of low quality, the output plan will be crap too. Visible `task_create` options include `model_profile`. +- **Create a plan** — Send a prompt; PlanExe starts creating the plan (typically takes 10–20 minutes on baseline profile). If the input prompt is of low quality, the output plan will be crap too. Visible `plan_create` options include `model_profile`. - **Check progress** — Ask for status and see how far the plan has gotten. -- **Retry failed runs** — If status is `failed`, call `task_retry` (defaults to baseline model profile) to requeue the same task id. +- **Retry failed runs** — If status is `failed`, call `plan_retry` (defaults to baseline model profile) to requeue the same task id. - **Download the report** — When the plan is ready, the user specifies whether to download the HTML report or the zip of intermediary files (JSON, MD, CSV). --- diff --git a/docs/mcp/planexe_mcp_interface.md b/docs/mcp/planexe_mcp_interface.md index 37e0f0845..5386e1ac1 100644 --- a/docs/mcp/planexe_mcp_interface.md +++ b/docs/mcp/planexe_mcp_interface.md @@ -8,14 +8,14 @@ PlanExe is a service that generates **strategic project-plan drafts** from a nat ### 1.2 What kind of plan does it create -The plan is a **project plan**: a DAG of steps (Luigi tasks) that produce artifacts including a Gantt chart, risk analysis, and other project management deliverables. The main output is a self-contained interactive HTML report (~700KB) with collapsible sections, interactive Gantt charts, and embedded JavaScript. The report contains 20+ sections including executive summary, investor pitch, project plan with SMART criteria, strategic decision analysis, scenario comparison, assumptions with expert review, governance structure, SWOT analysis, team role profiles, simulated expert criticism, work breakdown structure, plan review, Q&A, premortem with failure scenarios, self-audit checklist, and adversarial premise attacks. There is also a zip file containing all intermediary pipeline files (md, json, csv) that fed the report. Plan quality depends on prompt quality; use the prompt_examples tool to see the baseline before calling task_create. +The plan is a **project plan**: a DAG of steps (Luigi tasks) that produce artifacts including a Gantt chart, risk analysis, and other project management deliverables. The main output is a self-contained interactive HTML report (~700KB) with collapsible sections, interactive Gantt charts, and embedded JavaScript. The report contains 20+ sections including executive summary, investor pitch, project plan with SMART criteria, strategic decision analysis, scenario comparison, assumptions with expert review, governance structure, SWOT analysis, team role profiles, simulated expert criticism, work breakdown structure, plan review, Q&A, premortem with failure scenarios, self-audit checklist, and adversarial premise attacks. There is also a zip file containing all intermediary pipeline files (md, json, csv) that fed the report. Plan quality depends on prompt quality; use the prompt_examples tool to see the baseline before calling plan_create. #### 1.2.1 Agent-facing summary (for server instructions / tool descriptions) Implementors should expose the following to agents so they understand what PlanExe does: - **What:** PlanExe turns a plain-English goal into a strategic project-plan draft (20+ sections) in ~10–20 min. Sections include executive summary, interactive Gantt charts, investor pitch, SWOT, governance, team profiles, work breakdown, scenario comparison, expert criticism, and adversarial sections (premortem, self-audit, premise attacks) that stress-test the plan. The output is a draft to refine, not an executable or final document — but it surfaces hard questions the prompter may not have considered. -- **Required interaction order:** Call `prompt_examples` first. Optional before `task_create`: call `model_profiles` to inspect profile guidance and available models in each profile. Then complete a non-tool step: formulate a detailed prompt as flowing prose (not structured markdown), typically ~300-800 words, using the examples as a baseline; include objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria; get user approval. Only after approval, call `task_create`. Then poll `task_status` (about every 5 minutes); use `task_download` (mcp_local helper) or `task_file_info` (mcp_cloud tool) when complete (`pending`/`processing` = keep polling, `completed` = download now, `failed` = terminal). If a task fails and the caller wants another attempt for the same `task_id`, call `task_retry` (optional `model_profile`, default `baseline`). To stop, call `task_stop` with the `task_id` from `task_create`. +- **Required interaction order:** Call `prompt_examples` first. Optional before `plan_create`: call `model_profiles` to inspect profile guidance and available models in each profile. Then complete a non-tool step: formulate a detailed prompt as flowing prose (not structured markdown), typically ~300-800 words, using the examples as a baseline; include objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria; get user approval. Only after approval, call `plan_create`. Then poll `plan_status` (about every 5 minutes); use `plan_download` (mcp_local helper) or `plan_file_info` (mcp_cloud tool) when complete (`pending`/`processing` = keep polling, `completed` = download now, `failed` = terminal). If a task fails and the caller wants another attempt for the same `task_id`, call `plan_retry` (optional `model_profile`, default `baseline`). To stop, call `plan_stop` with the `task_id` from `plan_create`. - **Output:** Self-contained interactive HTML report (~700KB) with collapsible sections and interactive Gantt charts — open in a browser. The zip contains the intermediary pipeline files (md, json, csv) that fed the report. ### 1.3 Scope of this document @@ -70,10 +70,10 @@ The interface is designed to support: The MCP specification defines two different mechanisms: -- **MCP tools** (e.g. task_create, task_status, task_stop, task_retry): the server exposes named tools; the client calls them and receives a response. PlanExe's interface is **tool-based**: the agent calls task_create → receives task_id → polls task_status → optionally calls task_retry on failed → uses task_file_info (and optionally task_download via mcp_local). This document specifies those tools. +- **MCP tools** (e.g. plan_create, plan_status, plan_stop, plan_retry): the server exposes named tools; the client calls them and receives a response. PlanExe's interface is **tool-based**: the agent calls plan_create → receives task_id → polls plan_status → optionally calls plan_retry on failed → uses plan_file_info (and optionally plan_download via mcp_local). This document specifies those tools. - **MCP tasks protocol** ("Run as task" in some UIs): a separate mechanism where the client can run a tool "as a task" using RPC methods such as tasks/run, tasks/get, tasks/result, tasks/cancel, tasks/list, so the tool runs in the background and the client polls for results. -PlanExe **does not** use or advertise the MCP tasks protocol. Implementors and clients should use the **tools only**. Do not enable "Run as task" for PlanExe; many clients (e.g. Cursor) and the Python MCP SDK do not support the tasks protocol properly. Intended flow: call `prompt_examples`; optionally call `model_profiles`; perform the non-tool prompt drafting/approval step; call `task_create`; poll `task_status`; if failed call `task_retry` (optional); then call `task_file_info` (or `task_download` via mcp_local) when completed. +PlanExe **does not** use or advertise the MCP tasks protocol. Implementors and clients should use the **tools only**. Do not enable "Run as task" for PlanExe; many clients (e.g. Cursor) and the Python MCP SDK do not support the tasks protocol properly. Intended flow: call `prompt_examples`; optionally call `model_profiles`; perform the non-tool prompt drafting/approval step; call `plan_create`; poll `plan_status`; if failed call `plan_retry` (optional); then call `plan_file_info` (or `plan_download` via mcp_local) when completed. --- @@ -87,7 +87,7 @@ A long-lived container for a PlanExe project run. **Key properties** -- task_id: UUID returned by task_create for that task. Each task_create returns a new UUID. Use that exact UUID for all MCP calls; do not substitute ids from other services. +- task_id: UUID returned by plan_create for that task. Each plan_create returns a new UUID. Use that exact UUID for all MCP calls; do not substitute ids from other services. - output_dir: artifact root namespace for task - config: immutable run configuration (models, runtime limits, Luigi params) - created_at, updated_at @@ -142,7 +142,7 @@ The public MCP `state` field is aligned with `TaskItem.state`: - pending → processing when picked up by a worker - processing → completed via normal success - processing → failed via error -- failed → pending when `task_retry` is accepted +- failed → pending when `plan_retry` is accepted ### 5.3 Invalid transitions @@ -157,7 +157,7 @@ All tool names below are normative. ### 6.1 prompt_examples -**Call this first.** Returns example prompts that define the baseline for what a good prompt looks like. Do not call task_create yet. Correct flow: call this tool; optionally call `model_profiles`; then complete a non-tool step (draft and approve a detailed prompt, typically ~300-800 words); only then call `task_create`. If you call `task_create` before formulating and approving a prompt, the resulting plan will be lower quality than it could be. +**Call this first.** Returns example prompts that define the baseline for what a good prompt looks like. Do not call plan_create yet. Correct flow: call this tool; optionally call `model_profiles`; then complete a non-tool step (draft and approve a detailed prompt, typically ~300-800 words); only then call `plan_create`. If you call `plan_create` before formulating and approving a prompt, the resulting plan will be lower quality than it could be. Write the prompt as flowing prose, not structured markdown with headers or bullet lists. Weave technical specs, constraints, and targets naturally into sentences. Include banned words/approaches and governance structure inline. Typical length: 300–800 words. The examples demonstrate this prose style — match their tone and density. @@ -207,11 +207,11 @@ If no models are available in any profile, the tool returns `isError=true` with } ``` -Use the returned `profile` values directly in `task_create.model_profile`. +Use the returned `profile` values directly in `plan_create.model_profile`. --- -### 6.2 task_create +### 6.2 plan_create **Call only after prompt_examples and after the non-tool drafting/approval step.** Start creating a new plan with the approved prompt. @@ -263,18 +263,18 @@ Write as flowing prose, not structured markdown. Include banned approaches, gove Use a normal single LLM response (not PlanExe) for one-shot micro-tasks. PlanExe runs a heavy multi-step planning pipeline and is best for substantial project planning. -- Bad (do not send to task_create): "Give me a 5-point checklist for launching a coffee shop." +- Bad (do not send to plan_create): "Give me a 5-point checklist for launching a coffee shop." - Better non-PlanExe action: ask the LLM directly for a checklist. - Better PlanExe prompt: "Create a 12-month strategic launch plan for a coffee shop in Austin with budget caps, lease milestones, hiring plan, permits, supply chain, marketing channels, risk register, governance, and success KPIs." -- Bad (do not send to task_create): "Summarize this text in 6 bullets." +- Bad (do not send to plan_create): "Summarize this text in 6 bullets." - Better non-PlanExe action: use direct summarization in the chat model. - Bad (invalid assumption): "Run only the risk-register part of PlanExe." - Rule: PlanExe pipeline execution is fixed end-to-end. Callers cannot choose internal step subsets. - Better PlanExe prompt: request a full plan where risk analysis is one required deliverable. -- Bad (do not send to task_create): "Rewrite this email to sound professional." +- Bad (do not send to plan_create): "Rewrite this email to sound professional." - Better non-PlanExe action: use direct rewriting in the chat model. **Optional** @@ -282,7 +282,7 @@ Use a normal single LLM response (not PlanExe) for one-shot micro-tasks. PlanExe - model_profile: LLM profile (`baseline` | `premium` | `frontier` | `custom`). If unsure, call `model_profiles` first. - user_api_key: user API key for credits and attribution (if your deployment requires it). -Clients can call the MCP tool **prompt_examples** to retrieve example prompts. Use these as examples for task_create; they can also call task_create with any prompt—short prompts produce less detailed plans. +Clients can call the MCP tool **prompt_examples** to retrieve example prompts. Use these as examples for plan_create; they can also call plan_create with any prompt—short prompts produce less detailed plans. For the full catalog file: @@ -299,17 +299,17 @@ For the full catalog file: **Important** -- task_id is a UUID returned by task_create. Use this exact UUID for task_status/task_stop/task_retry/task_file_info (and task_download when using mcp_local). +- task_id is a UUID returned by plan_create. Use this exact UUID for plan_status/plan_stop/plan_retry/plan_file_info (and plan_download when using mcp_local). **Behavior** - Must be idempotent only if client supplies an optional client_request_id (optional extension). - Task config is immutable after creation in v1. -- By default, repeated `task_create` calls produce new tasks (new `task_id`s). +- By default, repeated `plan_create` calls produce new tasks (new `task_id`s). --- -### 6.3 task_status +### 6.3 plan_status Returns task status and progress. Used for progress bars and UI states. **Polling interval:** call at reasonable intervals only (e.g. every 5 minutes); plan generation typically takes 10–20 minutes (baseline profile) and may take longer on higher-quality profiles. @@ -323,7 +323,7 @@ Returns task status and progress. Used for progress bars and UI states. **Pollin **Input** -- task_id: UUID returned by task_create. Use it to reference the plan being created. +- task_id: UUID returned by plan_create. Use it to reference the plan being created. **Caller contract (state meanings)** @@ -362,9 +362,9 @@ Returns task status and progress. Used for progress bars and UI states. **Pollin --- -### 6.4 task_stop +### 6.4 plan_stop -Requests the plan generation to stop. Pass the **task_id** (the UUID returned by task_create). Call `task_stop` with that task_id. +Requests the plan generation to stop. Pass the **task_id** (the UUID returned by plan_create). Call `plan_stop` with that task_id. **Request** @@ -376,7 +376,7 @@ Requests the plan generation to stop. Pass the **task_id** (the UUID returned by **Input** -- task_id: UUID returned by task_create. Use this same UUID when calling task_stop to request the task to stop. +- task_id: UUID returned by plan_create. Use this same UUID when calling plan_stop to request the task to stop. **Response** @@ -394,7 +394,7 @@ Requests the plan generation to stop. Pass the **task_id** (the UUID returned by --- -### 6.5 task_retry +### 6.5 plan_retry Retries a task that is currently in `failed` state. @@ -436,18 +436,18 @@ Retries a task that is currently in `failed` state. --- -### 6.6 Download flow (task_download vs task_file_info) +### 6.6 Download flow (plan_download vs plan_file_info) -**If your client exposes task_download** (e.g. mcp_local): use it to save the report or zip locally; it calls task_file_info under the hood, then fetches and writes to the local save path (e.g. PLANEXE_PATH). +**If your client exposes plan_download** (e.g. mcp_local): use it to save the report or zip locally; it calls plan_file_info under the hood, then fetches and writes to the local save path (e.g. PLANEXE_PATH). -**If you only have task_file_info** (e.g. direct connection to mcp_cloud): call it with task_id and artifact ("report" or "zip"); use the returned download_url to fetch the file (e.g. GET with API key if configured). +**If you only have plan_file_info** (e.g. direct connection to mcp_cloud): call it with task_id and artifact ("report" or "zip"); use the returned download_url to fetch the file (e.g. GET with API key if configured). -**task_file_info input** +**plan_file_info input** -- task_id: UUID returned by task_create. Use it to download the created plan. +- task_id: UUID returned by plan_create. Use it to download the created plan. - artifact: "report" or "zip" (default "report"). -**task_download local path behavior (mcp_local)** +**plan_download local path behavior (mcp_local)** - Save directory is `PLANEXE_PATH`. - If `PLANEXE_PATH` is unset, save to current working directory. @@ -456,7 +456,7 @@ Retries a task that is currently in `failed` state. - If a filename already exists, append `-1`, `-2`, ... before extension. - Successful responses include `saved_path`. -**task_file_info URL behavior (mcp_cloud)** +**plan_file_info URL behavior (mcp_cloud)** - `download_url` is an absolute URL where the requested artifact can be downloaded. @@ -491,8 +491,8 @@ Recommended practice for MCP clients: Additional semantics: -- Every `task_create` call creates a new independent task with a new `task_id`. -- `task_retry` reuses the existing failed `task_id` (it does not create a new task id). +- Every `plan_create` call creates a new independent task with a new `task_id`. +- `plan_retry` reuses the existing failed `task_id` (it does not create a new task id). - The server does not deduplicate “same prompt” requests into a single shared task. - Keep your own task registry/client state if you run multiple tasks concurrently. @@ -521,9 +521,9 @@ Example: ### 9.2 isError behavior -- `task_create`, `task_status`, `task_stop`, `task_retry`: unknown/invalid requests return `isError=true` with `error`. +- `plan_create`, `plan_status`, `plan_stop`, `plan_retry`: unknown/invalid requests return `isError=true` with `error`. - `model_profiles`: returns `isError=true` with `MODEL_PROFILES_UNAVAILABLE` when no models are available in any profile. -- `task_file_info`: uses mixed behavior: +- `plan_file_info`: uses mixed behavior: - returns `{}` (not an error) while artifacts are not ready. - may return `{"error": ...}` with `isError=false` for terminal artifact-level problems. - returns `isError=true` for unknown task id (`TASK_NOT_FOUND`). @@ -536,13 +536,13 @@ Cloud/core tool codes: - `INVALID_TOOL`: unknown MCP tool name. - `INTERNAL_ERROR`: uncaught server error. - `TASK_NOT_FOUND`: task id not found. -- `TASK_NOT_FAILED`: task_retry called for a task that is not in failed state. +- `TASK_NOT_FAILED`: plan_retry called for a task that is not in failed state. - `INVALID_USER_API_KEY`: provided user_api_key is invalid. -- `USER_API_KEY_REQUIRED`: deployment requires user_api_key for task_create. -- `INSUFFICIENT_CREDITS`: caller account has no credits for task_create. +- `USER_API_KEY_REQUIRED`: deployment requires user_api_key for plan_create. +- `INSUFFICIENT_CREDITS`: caller account has no credits for plan_create. - `MODEL_PROFILES_UNAVAILABLE`: model_profiles found zero available models across all profiles. -- `generation_failed`: task_file_info report path when task ended in failed. -- `content_unavailable`: task_file_info cannot read requested artifact bytes. +- `generation_failed`: plan_file_info report path when task ended in failed. +- `content_unavailable`: plan_file_info cannot read requested artifact bytes. Local proxy specific codes: @@ -560,7 +560,7 @@ Local proxy specific codes: - `USER_API_KEY_REQUIRED` - `INSUFFICIENT_CREDITS` - `INVALID_TOOL` -- For `TASK_NOT_FAILED`: call `task_retry` only after `task_status.state == failed`. +- For `TASK_NOT_FAILED`: call `plan_retry` only after `plan_status.state == failed`. - For `TASK_NOT_FOUND`: verify task_id source and stop polling that id. - For `generation_failed`: treat as terminal failure and surface task progress_message to user. @@ -597,7 +597,7 @@ At minimum: ### 11.1 Responsiveness -- task_status must return within < 250ms under normal load. +- plan_status must return within < 250ms under normal load. ### 11.2 Large artifacts @@ -624,7 +624,7 @@ To match your UI behavior: Use: -- task_status.progress_percentage +- plan_status.progress_percentage - or progress_updated events --- @@ -689,7 +689,7 @@ PlanExe is artifact-first, and MCP already has a native concept for that: resour The following tools remove common UX friction without expanding the core model. -### 17.1 task_list (or task_recent) +### 17.1 plan_list (or task_recent) Return a short list of recent tasks so agents can recover if they lost a task_id. @@ -700,12 +700,12 @@ Return a short list of recent tasks so agents can recover if they lost a task_id ### 17.2 task_wait -Blocking helper that polls internally until the task completes or times out. Returns the final task_status payload plus suggested next steps. +Blocking helper that polls internally until the task completes or times out. Returns the final plan_status payload plus suggested next steps. **Notes** - Inputs: task_id, timeout_sec (optional), poll_interval_sec (optional). -- Outputs: same as task_status + next_steps (string or list). +- Outputs: same as plan_status + next_steps (string or list). ### 17.3 task_get_latest @@ -723,7 +723,7 @@ Return the tail of recent log lines for troubleshooting failures. **Notes** - Inputs: task_id, max_lines (optional), since_cursor (optional). -- Useful when task_status shows failed but no context. +- Useful when plan_status shows failed but no context. --- diff --git a/docs/proposals/73-rename-task-prefix-in-mcp-tools.md b/docs/proposals/73-rename-task-prefix-in-mcp-tools.md index 344fbc2f6..3e28c4ef8 100644 --- a/docs/proposals/73-rename-task-prefix-in-mcp-tools.md +++ b/docs/proposals/73-rename-task-prefix-in-mcp-tools.md @@ -2,7 +2,7 @@ ## Status -Draft +Implemented --- diff --git a/mcp_cloud/AGENTS.md b/mcp_cloud/AGENTS.md index 3ebd52308..5a3f35eea 100644 --- a/mcp_cloud/AGENTS.md +++ b/mcp_cloud/AGENTS.md @@ -17,25 +17,25 @@ for AI agents and developer tools to interact with PlanExe. Communicates with - Events are queried from `EventItem` database records. - Use the TaskItem UUID as the MCP `task_id`. - Public task state contract: - - `task_status.state` must use exactly: `pending`, `processing`, `completed`, `failed`. + - `plan_status.state` must use exactly: `pending`, `processing`, `completed`, `failed`. - These values correspond 1:1 with `database_api.model_taskitem.TaskState`. - - Do not use legacy public names like `running`, `stopping`, or `stopped` for `task_status`. + - Do not use legacy public names like `running`, `stopping`, or `stopped` for `plan_status`. - Do not expose internal symbol/class names (for example `TaskState.pending`, `TaskItem.state`) in model-facing tool descriptions; use plain public state strings. - Download contract: - `track_activity.jsonl` is internal-only (`TaskItem.run_track_activity_jsonl`). - Downloadable zip artifacts must never include `track_activity.jsonl`. - Serve new layout snapshots directly; sanitize only legacy/fallback zips. -- `task_stop` contract: - - `task_stop` does not create a separate lifecycle state. +- `plan_stop` contract: + - `plan_stop` does not create a separate lifecycle state. - Return current public `state` plus `stop_requested` to acknowledge stop-flag request. - Forbidden imports: `worker_plan.app`, `worker_plan_internal`, `frontend_*`, `open_dir_server`. -## task_create contract +## plan_create contract - Expose `model_profiles` as the discovery tool for profile selection. - `model_profiles` must report profile guidance and currently available models after class whitelist filtering. -- Keep workflow wording explicit that prompt drafting + user approval is a non-tool step before `task_create`. -- Keep concurrency wording explicit: each `task_create` call creates a new `task_id`; no global per-client concurrency cap is enforced server-side. +- Keep workflow wording explicit that prompt drafting + user approval is a non-tool step before `plan_create`. +- Keep concurrency wording explicit: each `plan_create` call creates a new `task_id`; no global per-client concurrency cap is enforced server-side. - Visible input schema is intentionally limited to: - `prompt` - `model_profile` (`baseline`, `premium`, `frontier`, `custom`) @@ -47,7 +47,7 @@ for AI agents and developer tools to interact with PlanExe. Communicates with - All tool responses must be JSON-serializable and follow the error model in the spec. - Keep tool error codes/docs aligned with actual runtime payloads (for example `TASK_NOT_FOUND`, `INVALID_USER_API_KEY`, `USER_API_KEY_REQUIRED`, `INSUFFICIENT_CREDITS`, `generation_failed`, `content_unavailable`, `INTERNAL_ERROR`). - Event cursors use format `cursor_{event_id}` for incremental polling. -- **Run as task**: We expose MCP **tools** only (task_create, task_status, task_stop, etc.), not the MCP **tasks** protocol (tasks/get, tasks/result, etc.). Do not advertise the tasks capability or add "Run as task" support; the spec and clients (e.g. Cursor) are aligned on tools-only. +- **Run as task**: We expose MCP **tools** only (plan_create, plan_status, plan_stop, etc.), not the MCP **tasks** protocol (tasks/get, tasks/result, etc.). Do not advertise the tasks capability or add "Run as task" support; the spec and clients (e.g. Cursor) are aligned on tools-only. ## Authentication Policy - PlanExe MCP cloud authentication is API-key header based. @@ -61,7 +61,7 @@ for AI agents and developer tools to interact with PlanExe. Communicates with - `tools/call` without API key is allowed **only** for free setup tools: - `model_profiles` - `prompt_examples` - - All other tool invocations (for example `task_create`) must remain API-key protected. + - All other tool invocations (for example `plan_create`) must remain API-key protected. - Keep auth-denial logging explicit (`Auth rejected: ...`) with method/path/user-agent and parsed JSON-RPC methods to make Railway debugging easier. ## HTTP Compatibility and Crawler Endpoints @@ -72,7 +72,7 @@ for AI agents and developer tools to interact with PlanExe. Communicates with - FastMCP session lifecycle lines like `Terminating session: None` are expected informational logs; do not treat them as application failures solely based on Railway’s log-level labeling. ## Download URL environment behavior -- `task_file_info.download_url` should be built from `PLANEXE_MCP_PUBLIC_BASE_URL` when set. +- `plan_file_info.download_url` should be built from `PLANEXE_MCP_PUBLIC_BASE_URL` when set. - If `PLANEXE_MCP_PUBLIC_BASE_URL` is unset in HTTP mode, use request host/scheme. - If no public base URL is available, `download_url` may be absent; document this and guide operators to set `PLANEXE_MCP_PUBLIC_BASE_URL`. @@ -82,9 +82,9 @@ for AI agents and developer tools to interact with PlanExe. Communicates with - the HTTP wrapper endpoint (`/mcp/tools/call`), or - the streamable MCP JSON-RPC endpoint (`/mcp`). - Tool-surface split must stay explicit: - - `mcp_cloud` exposes `task_file_info` (not `task_download`). - - `mcp_local` exposes `task_download` and implements it via cloud `task_file_info`. -- `task_file_info` provides download metadata that `mcp_local` uses to download + - `mcp_cloud` exposes `plan_file_info` (not `plan_download`). + - `mcp_local` exposes `plan_download` and implements it via cloud `plan_file_info`. +- `plan_file_info` provides download metadata that `mcp_local` uses to download artifacts via `/download/{task_id}/...`. ## Troubleshooting guidance (caller-facing text) @@ -106,4 +106,4 @@ for AI agents and developer tools to interact with PlanExe. Communicates with tests close to the changed logic. - Run focused tests from repo root, for example: - `python -m unittest mcp_cloud.tests.test_tool_surface_consistency` - - `python -m unittest mcp_cloud.tests.test_task_status_tool` + - `python -m unittest mcp_cloud.tests.test_plan_status_tool` diff --git a/mcp_cloud/README.md b/mcp_cloud/README.md index 1ad70e17f..937e0b617 100644 --- a/mcp_cloud/README.md +++ b/mcp_cloud/README.md @@ -14,8 +14,8 @@ mcp_cloud provides a standardized MCP interface for PlanExe's plan generation wo ## Run as task (MCP tasks protocol) -MCP has two ways to run long-running work: **tools** (what we use) and the **tasks** protocol ("Run as task" in some UIs). PlanExe uses **tools only**: `prompt_examples`, `model_profiles`, `task_create`, `task_status`, `task_stop`, `task_retry`, `task_file_info` (or `task_download` via `mcp_local`). The agent creates a task, polls status, retries on failed when needed, then downloads; that is the intended flow per `docs/mcp/planexe_mcp_interface.md`. We do not advertise or implement the MCP tasks protocol (tasks/get, tasks/result, etc.). Clients like Cursor do not support it properly—use the tools directly. -Workflow clarity: prompt drafting + user approval is a non-tool step between setup tools and `task_create`. +MCP has two ways to run long-running work: **tools** (what we use) and the **tasks** protocol ("Run as task" in some UIs). PlanExe uses **tools only**: `prompt_examples`, `model_profiles`, `plan_create`, `plan_status`, `plan_stop`, `plan_retry`, `plan_file_info` (or `plan_download` via `mcp_local`). The agent creates a task, polls status, retries on failed when needed, then downloads; that is the intended flow per `docs/mcp/planexe_mcp_interface.md`. We do not advertise or implement the MCP tasks protocol (tasks/get, tasks/result, etc.). Clients like Cursor do not support it properly—use the tools directly. +Workflow clarity: prompt drafting + user approval is a non-tool step between setup tools and `plan_create`. ## Client Choice Guide @@ -35,7 +35,7 @@ docker compose up ``` Important: `mcp_cloud` enqueues tasks and `worker_plan_database_{n}` executes them. -If no `worker_plan_database*` service is running, `task_create` returns a task id but the task will not progress. +If no `worker_plan_database*` service is running, `plan_create` returns a task id but the task will not progress. mcp_cloud exposes HTTP endpoints on port `8001` (or `${PLANEXE_MCP_HTTP_PORT}`). Authentication is controlled by `PLANEXE_MCP_REQUIRE_AUTH`: - `false`: no API key needed (local docker default). @@ -93,7 +93,7 @@ Some MCP clients (e.g. OpenClaw/mcporter) connect by doing a **GET** to the serv **You do not need SSE for tools.** MCP over HTTP can use plain JSON: - **List tools:** `GET http://:8001/mcp/tools` → returns `{"tools": [...]}` (JSON). -- **Call a tool:** `POST http://:8001/mcp/tools/call` with body `{"tool": "task_create", "arguments": {"prompt": "…"}}` → returns JSON. +- **Call a tool:** `POST http://:8001/mcp/tools/call` with body `{"tool": "plan_create", "arguments": {"prompt": "…"}}` → returns JSON. If your client only supports Streamable HTTP and fails on `/mcp`, you have two options: @@ -108,7 +108,7 @@ If your client only supports Streamable HTTP and fails on `/mcp`, you have two o - `PLANEXE_MCP_API_KEY`: Optional shared secret for auth. When auth is enabled, clients can use this key instead of a UserApiKey. For production with user accounts, keys from home.planexe.org (UserApiKey) are validated against the database. - `PLANEXE_MCP_HTTP_HOST`: HTTP server host (default: `127.0.0.1`). Use `0.0.0.0` to bind all interfaces (containers/cloud). - `PLANEXE_MCP_HTTP_PORT`: HTTP server port (default: `8001`). Railway will override with `PORT` env var. -- `PLANEXE_MCP_PUBLIC_BASE_URL`: Public base URL for report/zip download links in `task_file_info` (e.g. `http://192.168.1.40:8001`). When set, `download_url` is built from this value. When unset, the HTTP server uses the request’s host (scheme + authority), so clients connecting at `http://192.168.1.40:8001/mcp/` get download URLs like `http://192.168.1.40:8001/download/...` instead of localhost. If clients still see localhost in download URLs (e.g. behind a proxy), set this env var explicitly in `.env`. +- `PLANEXE_MCP_PUBLIC_BASE_URL`: Public base URL for report/zip download links in `plan_file_info` (e.g. `http://192.168.1.40:8001`). When set, `download_url` is built from this value. When unset, the HTTP server uses the request’s host (scheme + authority), so clients connecting at `http://192.168.1.40:8001/mcp/` get download URLs like `http://192.168.1.40:8001/download/...` instead of localhost. If clients still see localhost in download URLs (e.g. behind a proxy), set this env var explicitly in `.env`. - `PORT`: Railway-provided port (takes precedence over `PLANEXE_MCP_HTTP_PORT`) - `PLANEXE_MCP_CORS_ORIGINS`: Comma-separated list of allowed origins. When unset, uses `*` (all origins) so browser-based tools like the MCP Inspector can connect. If you set it (e.g. for a specific frontend), include `http://localhost:6274` and `http://127.0.0.1:6274` for the Inspector. - `PLANEXE_MCP_MAX_BODY_BYTES`: Max request size for `POST /mcp/tools/call` (default: `1048576`). @@ -131,36 +131,36 @@ mcp_cloud uses the same database configuration as other PlanExe services: See `docs/mcp/planexe_mcp_interface.md` for full specification. Available tools: -- `prompt_examples` - Return example prompts. Use these as examples for task_create. +- `prompt_examples` - Return example prompts. Use these as examples for plan_create. - `model_profiles` - List profile options and currently available models in each profile. -- `task_create` - Create a new task (returns task_id as UUID; may require user_api_key for credits) -- `task_status` - Get task status and progress -- `task_stop` - Stop an active task -- `task_retry` - Retry a failed task with the same task_id (optional model_profile, default baseline) -- `task_file_info` - Get file metadata for report or zip +- `plan_create` - Create a new task (returns task_id as UUID; may require user_api_key for credits) +- `plan_status` - Get task status and progress +- `plan_stop` - Stop an active task +- `plan_retry` - Retry a failed task with the same task_id (optional model_profile, default baseline) +- `plan_file_info` - Get file metadata for report or zip -`task_status` caller contract: +`plan_status` caller contract: - `pending` / `processing`: keep polling. - `completed`: terminal success, download is ready. - `failed`: terminal error. -- If `failed`, call `task_retry` to requeue the same task id. +- If `failed`, call `plan_retry` to requeue the same task id. Concurrency semantics: -- Each `task_create` call creates a new `task_id`. -- `task_retry` reuses the same failed `task_id`. +- Each `plan_create` call creates a new `task_id`. +- `plan_retry` reuses the same failed `task_id`. - Server does not enforce a global one-task-at-a-time cap per client. - Client should track task ids explicitly when running tasks in parallel. Minimal error contract: - Tool errors use `{"error":{"code","message","details?"}}`. - Common codes: `TASK_NOT_FOUND`, `TASK_NOT_FAILED`, `INVALID_USER_API_KEY`, `USER_API_KEY_REQUIRED`, `INSUFFICIENT_CREDITS`, `INTERNAL_ERROR`, `generation_failed`, `content_unavailable`. -- `task_file_info` may return `{}` while output is not ready (not an error payload). +- `plan_file_info` may return `{}` while output is not ready (not an error payload). -Note: `task_download` is a synthetic tool provided by `mcp_local`, not by this server. If your client exposes `task_download`, use it to save the report or zip locally; otherwise use `task_file_info` to get `download_url` and fetch the file yourself. +Note: `plan_download` is a synthetic tool provided by `mcp_local`, not by this server. If your client exposes `plan_download`, use it to save the report or zip locally; otherwise use `plan_file_info` to get `download_url` and fetch the file yourself. -**Tip**: Call `prompt_examples` to get example prompts to use with task_create, then call `model_profiles` to choose `model_profile` based on current runtime availability. The prompt catalog is the same as in the frontends (`worker_plan.worker_plan_api.PromptCatalog`). When running with `PYTHONPATH` set to the repo root (e.g. stdio setup), the catalog is loaded automatically; otherwise built-in examples are returned. +**Tip**: Call `prompt_examples` to get example prompts to use with plan_create, then call `model_profiles` to choose `model_profile` based on current runtime availability. The prompt catalog is the same as in the frontends (`worker_plan.worker_plan_api.PromptCatalog`). When running with `PYTHONPATH` set to the repo root (e.g. stdio setup), the catalog is loaded automatically; otherwise built-in examples are returned. -Download flow: call `task_file_info` to obtain the `download_url`, then fetch the +Download flow: call `plan_file_info` to obtain the `download_url`, then fetch the report via `GET /download/{task_id}/030-report.html` (API key required if configured). If `download_url` is missing, configure `PLANEXE_MCP_PUBLIC_BASE_URL` so the server can emit a reachable absolute URL. diff --git a/mcp_cloud/app.py b/mcp_cloud/app.py index b2e9e09bc..19ca6bae7 100644 --- a/mcp_cloud/app.py +++ b/mcp_cloud/app.py @@ -69,20 +69,27 @@ ModelProfilesOutput, PromptExamplesInput, PromptExamplesOutput, + PlanCreateInput, + PlanCreateOutput, + PlanRetryInput, + PlanRetryOutput, + PlanStopOutput, + PlanStatusInput, + PlanStopInput, + PlanFileInfoInput, + PlanFileInfoNotReadyOutput, + PlanStatusSuccess, + PlanFileInfoReadyOutput, + PlanListInput, + PlanListOutput, + ErrorDetail, + # backward-compat aliases used by internal Request classes TaskCreateInput, - TaskCreateOutput, - TaskRetryInput, - TaskRetryOutput, - TaskStopOutput, TaskStatusInput, TaskStopInput, + TaskRetryInput, TaskFileInfoInput, - TaskFileInfoNotReadyOutput, - TaskStatusSuccess, - TaskFileInfoReadyOutput, TaskListInput, - TaskListOutput, - ErrorDetail, ) app = Flask(__name__) @@ -141,23 +148,23 @@ def ensure_taskitem_stop_columns() -> None: "Do not use PlanExe for tiny one-shot outputs (for example: 'give me a 5-point checklist'); use a normal LLM response for that. " "The planning pipeline is fixed end-to-end; callers cannot select individual internal pipeline steps to run. " "Required interaction order: call prompt_examples first. " - "Optional before task_create: call model_profiles to see profile guidance and available models in each profile. " + "Optional before plan_create: call model_profiles to see profile guidance and available models in each profile. " "Then perform a non-tool step: draft a strong prompt as flowing prose (not structured markdown with headers or bullets), " "typically ~300-800 words, and get user approval. " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. " "Write the prompt as flowing prose — weave specs, constraints, and targets naturally into sentences. " - "Only after approval, call task_create. " - "Each task_create call creates a new task_id; the server does not enforce a global per-client concurrency limit. " - "Then poll task_status (about every 5 minutes); use task_file_info when complete. " - "If a run fails, call task_retry with the failed task_id to requeue it (optional model_profile, defaults to baseline). " - "To stop, call task_stop with the task_id from task_create; stopping is asynchronous and the task will eventually transition to failed. " + "Only after approval, call plan_create. " + "Each plan_create call creates a new task_id; the server does not enforce a global per-client concurrency limit. " + "Then poll plan_status (about every 5 minutes); use plan_file_info when complete. " + "If a run fails, call plan_retry with the failed task_id to requeue it (optional model_profile, defaults to baseline). " + "To stop, call plan_stop with the task_id from plan_create; stopping is asynchronous and the task will eventually transition to failed. " "If model_profiles returns MODEL_PROFILES_UNAVAILABLE, inform the user that no models are currently configured and the server administrator needs to set up model profiles. " - "Tool errors use {error:{code,message}}. task_file_info returns {ready:false,reason:...} while the artifact is not yet ready; check readiness by testing whether download_url is present in the response. " - "task_file_info download_url is the absolute URL where the requested artifact can be downloaded. " - "To list recent tasks for a user call task_list with user_api_key; returns task_id, state, progress_percentage, created_at, and prompt_excerpt for each task. " - "task_status state contract: pending/processing => keep polling; completed => download is ready; failed => terminal error. " - "Troubleshooting: if task_status stays in pending for longer than 5 minutes, the task was likely queued but not picked up by a worker (server issue). " - "If task_status is in processing and output files do not change for longer than 20 minutes, the task_create likely failed/stalled. " + "Tool errors use {error:{code,message}}. plan_file_info returns {ready:false,reason:...} while the artifact is not yet ready; check readiness by testing whether download_url is present in the response. " + "plan_file_info download_url is the absolute URL where the requested artifact can be downloaded. " + "To list recent tasks for a user call plan_list with user_api_key; returns task_id, state, progress_percentage, created_at, and prompt_excerpt for each task. " + "plan_status state contract: pending/processing => keep polling; completed => download is ready; failed => terminal error. " + "Troubleshooting: if plan_status stays in pending for longer than 5 minutes, the task was likely queued but not picked up by a worker (server issue). " + "If plan_status is in processing and output files do not change for longer than 20 minutes, the plan_create likely failed/stalled. " "In both cases, report the issue to PlanExe developers on GitHub: https://github.com/PlanExeOrg/PlanExe/issues . " "Main output: a self-contained interactive HTML report (~700KB) with collapsible sections and interactive Gantt charts — open in a browser. " "The zip contains the intermediary pipeline files (md, json, csv) that fed the report." @@ -304,7 +311,7 @@ def _create_task_sync( with app.app_context(): parameters = dict(config or {}) parameters["model_profile"] = normalize_model_profile(parameters.get("model_profile")).value - parameters["trigger_source"] = "mcp task_create" + parameters["trigger_source"] = "mcp plan_create" task = TaskItem( prompt=prompt, @@ -390,7 +397,7 @@ def _retry_failed_task_sync(task_id: str, model_profile: str) -> Optional[dict[s now_utc = datetime.now(UTC) parameters = dict(task.parameters) if isinstance(task.parameters, dict) else {} parameters["model_profile"] = normalized_profile - parameters["trigger_source"] = "mcp task_retry" + parameters["trigger_source"] = "mcp plan_retry" # Reset task state and clear prior run artifacts before requeueing. task.state = TaskState.pending @@ -758,7 +765,7 @@ def get_task_state_mapping(task_state: TaskState) -> str: return mapping.get(task_state, "pending") def _extract_task_create_metadata_overrides(arguments: dict[str, Any]) -> dict[str, Any]: - """Extract task_create runtime overrides from hidden metadata containers. + """Extract plan_create runtime overrides from hidden metadata containers. Supported hidden containers: - arguments.tool_metadata @@ -766,8 +773,9 @@ def _extract_task_create_metadata_overrides(arguments: dict[str, Any]) -> dict[s - arguments._meta If a container includes nested namespaces, these are checked first: - - task_create - - planexe_task_create + - plan_create + - task_create (legacy alias) + - planexe_task_create (legacy alias) - planexe """ merged: dict[str, Any] = {} @@ -780,7 +788,7 @@ def _extract_task_create_metadata_overrides(arguments: dict[str, Any]) -> dict[s for candidate in metadata_candidates: merged.update(candidate) - for nested_key in ("task_create", "planexe_task_create", "planexe"): + for nested_key in ("plan_create", "task_create", "planexe_task_create", "planexe"): nested = candidate.get(nested_key) if isinstance(nested, dict): merged.update(nested) @@ -918,7 +926,7 @@ def _get_model_profiles_sync() -> dict[str, Any]: "default_profile": default_profile, "profiles": profiles, "message": ( - "Use one of these profile values in task_create.model_profile. " + "Use one of these profile values in plan_create.model_profile. " "Model lists show what is currently available in each profile." ), } @@ -1110,45 +1118,59 @@ def _builtin_mcp_example_prompts() -> list[str]: ] -TASK_CREATE_INPUT_SCHEMA = TaskCreateInput.model_json_schema() -TASK_CREATE_OUTPUT_SCHEMA = TaskCreateOutput.model_json_schema() -TASK_STATUS_SUCCESS_SCHEMA = TaskStatusSuccess.model_json_schema() -TASK_STATUS_OUTPUT_SCHEMA = { +PLAN_CREATE_INPUT_SCHEMA = PlanCreateInput.model_json_schema() +PLAN_CREATE_OUTPUT_SCHEMA = PlanCreateOutput.model_json_schema() +PLAN_STATUS_SUCCESS_SCHEMA = PlanStatusSuccess.model_json_schema() +PLAN_STATUS_OUTPUT_SCHEMA = { "oneOf": [ { "type": "object", "properties": {"error": ErrorDetail.model_json_schema()}, "required": ["error"], }, - TASK_STATUS_SUCCESS_SCHEMA, + PLAN_STATUS_SUCCESS_SCHEMA, ] } -TASK_STOP_OUTPUT_SCHEMA = TaskStopOutput.model_json_schema() -TASK_RETRY_OUTPUT_SCHEMA = TaskRetryOutput.model_json_schema() -TASK_FILE_INFO_READY_OUTPUT_SCHEMA = TaskFileInfoReadyOutput.model_json_schema() -TASK_FILE_INFO_NOT_READY_OUTPUT_SCHEMA = TaskFileInfoNotReadyOutput.model_json_schema() -TASK_FILE_INFO_OUTPUT_SCHEMA = { +PLAN_STOP_OUTPUT_SCHEMA = PlanStopOutput.model_json_schema() +PLAN_RETRY_OUTPUT_SCHEMA = PlanRetryOutput.model_json_schema() +PLAN_FILE_INFO_READY_OUTPUT_SCHEMA = PlanFileInfoReadyOutput.model_json_schema() +PLAN_FILE_INFO_NOT_READY_OUTPUT_SCHEMA = PlanFileInfoNotReadyOutput.model_json_schema() +PLAN_FILE_INFO_OUTPUT_SCHEMA = { "oneOf": [ { "type": "object", "properties": {"error": ErrorDetail.model_json_schema()}, "required": ["error"], }, - TASK_FILE_INFO_NOT_READY_OUTPUT_SCHEMA, - TASK_FILE_INFO_READY_OUTPUT_SCHEMA, + PLAN_FILE_INFO_NOT_READY_OUTPUT_SCHEMA, + PLAN_FILE_INFO_READY_OUTPUT_SCHEMA, ] } -TASK_STATUS_INPUT_SCHEMA = TaskStatusInput.model_json_schema() -TASK_STOP_INPUT_SCHEMA = TaskStopInput.model_json_schema() -TASK_RETRY_INPUT_SCHEMA = TaskRetryInput.model_json_schema() -TASK_FILE_INFO_INPUT_SCHEMA = TaskFileInfoInput.model_json_schema() +PLAN_STATUS_INPUT_SCHEMA = PlanStatusInput.model_json_schema() +PLAN_STOP_INPUT_SCHEMA = PlanStopInput.model_json_schema() +PLAN_RETRY_INPUT_SCHEMA = PlanRetryInput.model_json_schema() +PLAN_FILE_INFO_INPUT_SCHEMA = PlanFileInfoInput.model_json_schema() PROMPT_EXAMPLES_INPUT_SCHEMA = PromptExamplesInput.model_json_schema() PROMPT_EXAMPLES_OUTPUT_SCHEMA = PromptExamplesOutput.model_json_schema() MODEL_PROFILES_INPUT_SCHEMA = ModelProfilesInput.model_json_schema() MODEL_PROFILES_OUTPUT_SCHEMA = ModelProfilesOutput.model_json_schema() -TASK_LIST_INPUT_SCHEMA = TaskListInput.model_json_schema() -TASK_LIST_OUTPUT_SCHEMA = TaskListOutput.model_json_schema() +PLAN_LIST_INPUT_SCHEMA = PlanListInput.model_json_schema() +PLAN_LIST_OUTPUT_SCHEMA = PlanListOutput.model_json_schema() + +# Backward-compatible aliases for tests that reference old TASK_* names +TASK_CREATE_INPUT_SCHEMA = PLAN_CREATE_INPUT_SCHEMA +TASK_CREATE_OUTPUT_SCHEMA = PLAN_CREATE_OUTPUT_SCHEMA +TASK_STATUS_INPUT_SCHEMA = PLAN_STATUS_INPUT_SCHEMA +TASK_STATUS_OUTPUT_SCHEMA = PLAN_STATUS_OUTPUT_SCHEMA +TASK_STOP_INPUT_SCHEMA = PLAN_STOP_INPUT_SCHEMA +TASK_STOP_OUTPUT_SCHEMA = PLAN_STOP_OUTPUT_SCHEMA +TASK_RETRY_INPUT_SCHEMA = PLAN_RETRY_INPUT_SCHEMA +TASK_RETRY_OUTPUT_SCHEMA = PLAN_RETRY_OUTPUT_SCHEMA +TASK_FILE_INFO_INPUT_SCHEMA = PLAN_FILE_INFO_INPUT_SCHEMA +TASK_FILE_INFO_OUTPUT_SCHEMA = PLAN_FILE_INFO_OUTPUT_SCHEMA +TASK_LIST_INPUT_SCHEMA = PLAN_LIST_INPUT_SCHEMA +TASK_LIST_OUTPUT_SCHEMA = PLAN_LIST_OUTPUT_SCHEMA @dataclass(frozen=True) class ToolDefinition: @@ -1163,13 +1185,13 @@ class ToolDefinition: name="prompt_examples", description=( "Call this first. Returns example prompts that define what a good prompt looks like. " - "Do NOT call task_create yet. Optional before task_create: call model_profiles to choose model_profile. " + "Do NOT call plan_create yet. Optional before plan_create: call model_profiles to choose model_profile. " "Next is a non-tool step: formulate a detailed prompt (typically ~300-800 words; use examples as a baseline, similar structure) and get user approval. " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. " "Write the prompt as flowing prose, not structured markdown with headers or bullet lists. " "Weave technical specs, constraints, and targets naturally into sentences. Include banned words/approaches and governance preferences inline. " "The examples demonstrate this prose style — match their tone and density. " - "Then call task_create. " + "Then call plan_create. " "PlanExe is not for tiny one-shot outputs like a 5-point checklist; and it does not support selecting only some internal pipeline steps." ), input_schema=PROMPT_EXAMPLES_INPUT_SCHEMA, @@ -1184,7 +1206,7 @@ class ToolDefinition: ToolDefinition( name="model_profiles", description=( - "Optional helper before task_create. Returns model_profile options with plain-language guidance " + "Optional helper before plan_create. Returns model_profile options with plain-language guidance " "and currently available models in each profile. " "If no models are available, returns error code MODEL_PROFILES_UNAVAILABLE." ), @@ -1198,7 +1220,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_create", + name="plan_create", description=( "Call only after prompt_examples and after you have completed prompt drafting/approval (non-tool step). " "PlanExe turns the approved prompt into a strategic project-plan draft (20+ sections) in ~10-20 min. " @@ -1208,15 +1230,15 @@ class ToolDefinition: "plan review (critical issues, KPIs, financial strategy, automation opportunities), Q&A, " "premortem with failure scenarios, self-audit checklist, and adversarial premise attacks that argue against the project. " "The adversarial sections (premortem, self-audit, premise attacks) surface risks and questions the prompter may not have considered. " - "Returns task_id (UUID); use it for task_status, task_stop, task_retry, and task_file_info. " - "If you lose a task_id, call task_list with your user_api_key to recover it. " - "Each task_create call creates a new task_id (no server-side dedup). " + "Returns task_id (UUID); use it for plan_status, plan_stop, plan_retry, and plan_file_info. " + "If you lose a task_id, call plan_list with your user_api_key to recover it. " + "Each plan_create call creates a new task_id (no server-side dedup). " "If you are unsure which model_profile to choose, call model_profiles first. " "If your deployment uses credits, include user_api_key to charge the correct account. " "Common error codes: INVALID_USER_API_KEY, USER_API_KEY_REQUIRED, INSUFFICIENT_CREDITS." ), - input_schema=TASK_CREATE_INPUT_SCHEMA, - output_schema=TASK_CREATE_OUTPUT_SCHEMA, + input_schema=PLAN_CREATE_INPUT_SCHEMA, + output_schema=PLAN_CREATE_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": False, @@ -1225,7 +1247,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_status", + name="plan_status", description=( "Returns status and progress of the plan currently being created. " "Poll at reasonable intervals only (e.g. every 5 minutes): plan generation typically takes 10-20 minutes " @@ -1238,8 +1260,8 @@ class ToolDefinition: "processing with no file-output changes for >20 minutes likely means failed/stalled. " "Report these issues to https://github.com/PlanExeOrg/PlanExe/issues ." ), - input_schema=TASK_STATUS_INPUT_SCHEMA, - output_schema=TASK_STATUS_OUTPUT_SCHEMA, + input_schema=PLAN_STATUS_INPUT_SCHEMA, + output_schema=PLAN_STATUS_OUTPUT_SCHEMA, annotations={ "readOnlyHint": True, "destructiveHint": False, @@ -1248,16 +1270,16 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_stop", + name="plan_stop", description=( - "Request the plan generation to stop. Pass the task_id (the UUID returned by task_create). " + "Request the plan generation to stop. Pass the task_id (the UUID returned by plan_create). " "Stopping is asynchronous: the stop flag is set immediately but the task may continue briefly before halting. " "A stopped task will eventually transition to the failed state. " "If the task is already completed or failed, stop_requested returns false (the task already finished). " "Unknown task_id returns error code TASK_NOT_FOUND." ), - input_schema=TASK_STOP_INPUT_SCHEMA, - output_schema=TASK_STOP_OUTPUT_SCHEMA, + input_schema=PLAN_STOP_INPUT_SCHEMA, + output_schema=PLAN_STOP_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": True, @@ -1266,15 +1288,15 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_retry", + name="plan_retry", description=( "Retry a task that is currently in failed state. " "Pass the failed task_id and optionally model_profile (defaults to baseline). " "The task is reset to pending, prior artifacts are cleared, and the same task_id is requeued for processing. " "Returns TASK_NOT_FOUND when task_id is unknown and TASK_NOT_FAILED when the task is not in failed state." ), - input_schema=TASK_RETRY_INPUT_SCHEMA, - output_schema=TASK_RETRY_OUTPUT_SCHEMA, + input_schema=PLAN_RETRY_INPUT_SCHEMA, + output_schema=PLAN_RETRY_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": False, @@ -1283,7 +1305,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_file_info", + name="plan_file_info", description=( "Returns file metadata (content_type, download_url, download_size) for the report or zip artifact. " "Use artifact='report' (default) for the interactive HTML report (~700KB, self-contained with embedded JS " @@ -1292,12 +1314,12 @@ class ToolDefinition: "While the task is still pending or processing, returns {ready:false,reason:\"processing\"}. " "Check readiness by testing whether download_url is present in the response. " "Once ready, present download_url to the user or fetch and save the file locally. " - "If your client exposes task_download (e.g. mcp_local), prefer that to save the file locally. " + "If your client exposes plan_download (e.g. mcp_local), prefer that to save the file locally. " "Terminal error codes: generation_failed (plan failed), content_unavailable (artifact missing). " "Unknown task_id returns error code TASK_NOT_FOUND." ), - input_schema=TASK_FILE_INFO_INPUT_SCHEMA, - output_schema=TASK_FILE_INFO_OUTPUT_SCHEMA, + input_schema=PLAN_FILE_INFO_INPUT_SCHEMA, + output_schema=PLAN_FILE_INFO_OUTPUT_SCHEMA, annotations={ "readOnlyHint": True, "destructiveHint": False, @@ -1306,7 +1328,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_list", + name="plan_list", description=( "List the most recent tasks for an authenticated user. " "Requires user_api_key (pex_...). " @@ -1314,8 +1336,8 @@ class ToolDefinition: "progress_percentage, created_at (ISO 8601), and a prompt_excerpt (first 100 chars). " "Use this to recover a lost task_id or to review recent activity." ), - input_schema=TASK_LIST_INPUT_SCHEMA, - output_schema=TASK_LIST_OUTPUT_SCHEMA, + input_schema=PLAN_LIST_INPUT_SCHEMA, + output_schema=PLAN_LIST_OUTPUT_SCHEMA, annotations={ "readOnlyHint": True, "destructiveHint": False, @@ -1361,7 +1383,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> CallToolResu isError=True, ) -async def handle_task_create(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_create(arguments: dict[str, Any]) -> CallToolResult: """Create a new PlanExe task and enqueue it for processing. Examples: @@ -1396,7 +1418,7 @@ async def handle_task_create(arguments: dict[str, Any]) -> CallToolResult: isError=True, ) elif require_user_key: - response = {"error": {"code": "USER_API_KEY_REQUIRED", "message": "user_api_key is required for task_create."}} + response = {"error": {"code": "USER_API_KEY_REQUIRED", "message": "user_api_key is required for plan_create."}} return CallToolResult( content=[TextContent(type="text", text=json.dumps(response))], structuredContent=response, @@ -1435,7 +1457,7 @@ async def handle_prompt_examples(arguments: dict[str, Any]) -> CallToolResult: "Write the prompt as flowing prose, not structured markdown with headers or bullet lists. " "Weave technical specs, constraints, and targets naturally into sentences. Include banned words/approaches and governance preferences inline. " "The examples demonstrate this prose style — match their tone and density. " - "Only after approval, call task_create. " + "Only after approval, call plan_create. " "Do not use PlanExe for tiny one-shot requests (e.g., rewrite this email, summarize this document). " "PlanExe always runs the full fixed planning pipeline; callers cannot run only selected internal steps." ), @@ -1474,14 +1496,14 @@ async def handle_model_profiles(arguments: dict[str, Any]) -> CallToolResult: ) -async def handle_task_status(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_status(arguments: dict[str, Any]) -> CallToolResult: """Fetch the current task status, progress, and recent files for a task. Examples: - {"task_id": "uuid"} → state/progress/timing + recent files Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. Returns: - content: JSON string matching structuredContent. @@ -1555,14 +1577,14 @@ async def handle_task_status(arguments: dict[str, Any]) -> CallToolResult: isError=False, ) -async def handle_task_stop(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_stop(arguments: dict[str, Any]) -> CallToolResult: """Request an active task to stop. Examples: - {"task_id": "uuid"} → stop request accepted Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. Returns: - content: JSON string matching structuredContent. @@ -1595,7 +1617,7 @@ async def handle_task_stop(arguments: dict[str, Any]) -> CallToolResult: ) -async def handle_task_retry(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_retry(arguments: dict[str, Any]) -> CallToolResult: """Retry a failed task by resetting it back to pending.""" req = TaskRetryRequest(**arguments) task_id = req.task_id @@ -1630,7 +1652,7 @@ async def handle_task_retry(arguments: dict[str, Any]) -> CallToolResult: ) -async def handle_task_file_info(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_file_info(arguments: dict[str, Any]) -> CallToolResult: """Return download metadata for a task's report or zip artifact. Examples: @@ -1638,7 +1660,7 @@ async def handle_task_file_info(arguments: dict[str, Any]) -> CallToolResult: - {"task_id": "uuid", "artifact": "zip"} → zip metadata Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. - artifact: Optional "report" or "zip". Returns: @@ -1751,7 +1773,7 @@ async def handle_task_file_info(arguments: dict[str, Any]) -> CallToolResult: isError=False, ) -async def handle_task_list(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_list(arguments: dict[str, Any]) -> CallToolResult: """Return recent tasks for an authenticated user.""" try: req = TaskListRequest(**arguments) @@ -1784,16 +1806,24 @@ async def handle_task_list(arguments: dict[str, Any]) -> CallToolResult: TOOL_HANDLERS = { - "task_create": handle_task_create, - "task_status": handle_task_status, - "task_stop": handle_task_stop, - "task_retry": handle_task_retry, - "task_file_info": handle_task_file_info, - "task_list": handle_task_list, + "plan_create": handle_plan_create, + "plan_status": handle_plan_status, + "plan_stop": handle_plan_stop, + "plan_retry": handle_plan_retry, + "plan_file_info": handle_plan_file_info, + "plan_list": handle_plan_list, "prompt_examples": handle_prompt_examples, "model_profiles": handle_model_profiles, } +# Backward-compatible aliases so existing imports of handle_task_* still work +handle_task_create = handle_plan_create +handle_task_status = handle_plan_status +handle_task_stop = handle_plan_stop +handle_task_retry = handle_plan_retry +handle_task_file_info = handle_plan_file_info +handle_task_list = handle_plan_list + async def main(): """Main entry point for MCP server.""" logger.info("Starting PlanExe MCP Cloud...") diff --git a/mcp_cloud/http_server.py b/mcp_cloud/http_server.py index e5f1ce929..c1ea7f7d1 100644 --- a/mcp_cloud/http_server.py +++ b/mcp_cloud/http_server.py @@ -26,11 +26,11 @@ from mcp_cloud.http_utils import strip_redundant_content from mcp_cloud.tool_models import ( ModelProfilesOutput, - TaskCreateOutput, - TaskFileInfoOutput, - TaskRetryOutput, - TaskStatusOutput, - TaskStopOutput, + PlanCreateOutput, + PlanFileInfoOutput, + PlanRetryOutput, + PlanStatusOutput, + PlanStopOutput, ) from mcp_cloud.dotenv_utils import load_planexe_dotenv @@ -57,13 +57,13 @@ clear_download_base_url, fetch_artifact_from_worker_plan, fetch_user_downloadable_zip, - handle_task_create, - handle_task_list, + handle_plan_create, + handle_plan_list, handle_model_profiles, - handle_task_status, - handle_task_retry, - handle_task_stop, - handle_task_file_info, + handle_plan_status, + handle_plan_retry, + handle_plan_stop, + handle_plan_file_info, handle_prompt_examples, resolve_task_for_task_id, set_download_base_url, @@ -576,13 +576,13 @@ def _normalize_tool_result(result: Any) -> tuple[list[dict[str, Any]], Optional[ ResultArtifactInput = Literal["report", "zip"] -async def task_create( +async def plan_create( prompt: str, model_profile: Annotated[ ModelProfileInput, Field(description="Model profile: baseline, premium, frontier, custom. Call model_profiles to inspect options."), ] = "baseline", -) -> Annotated[CallToolResult, TaskCreateOutput]: +) -> Annotated[CallToolResult, PlanCreateOutput]: """Create a new PlanExe task. Use prompt_examples first for example prompts.""" authenticated_user_api_key = _get_authenticated_user_api_key() arguments: dict[str, Any] = { @@ -591,41 +591,41 @@ async def task_create( } if authenticated_user_api_key: arguments["user_api_key"] = authenticated_user_api_key - return await handle_task_create( + return await handle_plan_create( arguments, ) -async def task_status( - task_id: str = Field(..., description="Task UUID returned by task_create."), -) -> Annotated[CallToolResult, TaskStatusOutput]: - return await handle_task_status({"task_id": task_id}) +async def plan_status( + task_id: str = Field(..., description="Task UUID returned by plan_create."), +) -> Annotated[CallToolResult, PlanStatusOutput]: + return await handle_plan_status({"task_id": task_id}) -async def task_stop( - task_id: str = Field(..., description="Task UUID returned by task_create. Use it to stop the plan creation."), -) -> Annotated[CallToolResult, TaskStopOutput]: - return await handle_task_stop({"task_id": task_id}) +async def plan_stop( + task_id: str = Field(..., description="Task UUID returned by plan_create. Use it to stop the plan creation."), +) -> Annotated[CallToolResult, PlanStopOutput]: + return await handle_plan_stop({"task_id": task_id}) -async def task_retry( +async def plan_retry( task_id: str = Field(..., description="UUID of the failed task to retry."), model_profile: Annotated[ ModelProfileInput, Field(description="Model profile used for retry. Defaults to baseline."), ] = "baseline", -) -> Annotated[CallToolResult, TaskRetryOutput]: - return await handle_task_retry({"task_id": task_id, "model_profile": model_profile}) +) -> Annotated[CallToolResult, PlanRetryOutput]: + return await handle_plan_retry({"task_id": task_id, "model_profile": model_profile}) -async def task_file_info( - task_id: str = Field(..., description="Task UUID returned by task_create. Use it to download the created plan."), +async def plan_file_info( + task_id: str = Field(..., description="Task UUID returned by plan_create. Use it to download the created plan."), artifact: Annotated[ ResultArtifactInput, Field(description="Download artifact type: report or zip."), ] = "report", -) -> Annotated[CallToolResult, TaskFileInfoOutput]: - return await handle_task_file_info({"task_id": task_id, "artifact": artifact}) +) -> Annotated[CallToolResult, PlanFileInfoOutput]: + return await handle_plan_file_info({"task_id": task_id, "artifact": artifact}) async def prompt_examples() -> CallToolResult: @@ -640,11 +640,11 @@ async def model_profiles() -> Annotated[CallToolResult, ModelProfilesOutput]: def _register_tools(server: FastMCP) -> None: handler_map = { - "task_create": task_create, - "task_status": task_status, - "task_stop": task_stop, - "task_retry": task_retry, - "task_file_info": task_file_info, + "plan_create": plan_create, + "plan_status": plan_status, + "plan_stop": plan_stop, + "plan_retry": plan_retry, + "plan_file_info": plan_file_info, "prompt_examples": prompt_examples, "model_profiles": model_profiles, } @@ -845,14 +845,14 @@ async def call_tool( This endpoint wraps the stdio-based MCP tool handlers for HTTP access. """ arguments = dict(payload.arguments or {}) - if payload.tool == "task_create": + if payload.tool == "plan_create": authenticated_user_api_key = _get_authenticated_user_api_key() if authenticated_user_api_key and not arguments.get("user_api_key"): arguments["user_api_key"] = authenticated_user_api_key if isinstance(payload.metadata, dict): arguments["metadata"] = dict(payload.metadata) - result = await handle_task_create(arguments) + result = await handle_plan_create(arguments) content, error = _normalize_tool_result(result) return MCPToolCallResponse(content=content, error=error) diff --git a/mcp_cloud/tests/test_cors_config.py b/mcp_cloud/tests/test_cors_config.py index 7cd7ef47a..998e0f430 100644 --- a/mcp_cloud/tests/test_cors_config.py +++ b/mcp_cloud/tests/test_cors_config.py @@ -95,7 +95,7 @@ def test_public_mcp_post_for_redirect_probe(self): headers={}, method="POST", path="/mcp", - body=b'{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"task_create","arguments":{"prompt":"x"}}}', + body=b'{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"plan_create","arguments":{"prompt":"x"}}}', ) result = asyncio.run(http_server._is_public_mcp_request_without_auth(request)) self.assertTrue(result) @@ -155,7 +155,7 @@ def test_non_public_streamable_tools_call(self): headers={}, method="POST", path="/mcp/", - body=b'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"task_create","arguments":{"prompt":"x"}}}', + body=b'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plan_create","arguments":{"prompt":"x"}}}', ) result = asyncio.run(http_server._is_public_mcp_request_without_auth(request)) self.assertFalse(result) @@ -190,12 +190,12 @@ def test_public_rest_tools_call_model_profiles(self): result = asyncio.run(http_server._is_public_mcp_request_without_auth(request)) self.assertTrue(result) - def test_non_public_rest_tools_call_task_create(self): + def test_non_public_rest_tools_call_plan_create(self): request = _RequestStub( headers={}, method="POST", path="/mcp/tools/call", - body=b'{"tool":"task_create","arguments":{"prompt":"x"}}', + body=b'{"tool":"plan_create","arguments":{"prompt":"x"}}', ) result = asyncio.run(http_server._is_public_mcp_request_without_auth(request)) self.assertFalse(result) diff --git a/mcp_cloud/tests/test_task_create_tool.py b/mcp_cloud/tests/test_task_create_tool.py index 1a1bafffb..6073183bd 100644 --- a/mcp_cloud/tests/test_task_create_tool.py +++ b/mcp_cloud/tests/test_task_create_tool.py @@ -6,13 +6,13 @@ from unittest.mock import MagicMock, patch from mcp.types import CallToolResult -from mcp_cloud.app import handle_list_tools, handle_task_create +from mcp_cloud.app import handle_list_tools, handle_plan_create as handle_task_create class TestTaskCreateTool(unittest.TestCase): def test_task_create_visible_schema_exposes_prompt_and_model_profile(self): tools = asyncio.run(handle_list_tools()) - task_create_tool = next(tool for tool in tools if tool.name == "task_create") + task_create_tool = next(tool for tool in tools if tool.name == "plan_create") properties = task_create_tool.inputSchema.get("properties", {}) self.assertIn("prompt", properties) self.assertIn("model_profile", properties) diff --git a/mcp_cloud/tests/test_task_file_info_tool.py b/mcp_cloud/tests/test_task_file_info_tool.py index 016656e81..4c57b20f2 100644 --- a/mcp_cloud/tests/test_task_file_info_tool.py +++ b/mcp_cloud/tests/test_task_file_info_tool.py @@ -11,7 +11,7 @@ ZIP_CONTENT_TYPE, _sanitize_legacy_zip_snapshot, extract_file_from_zip_bytes, - handle_task_file_info, + handle_plan_file_info as handle_task_file_info, handle_list_tools, list_files_from_zip_bytes, ) @@ -21,7 +21,7 @@ class TestTaskFileInfoTool(unittest.TestCase): def test_task_file_info_tool_listed(self): tools = asyncio.run(handle_list_tools()) tool_names = {tool.name for tool in tools} - self.assertIn("task_file_info", tool_names) + self.assertIn("plan_file_info", tool_names) def test_zip_helpers(self): buffer = BytesIO() diff --git a/mcp_cloud/tests/test_task_retry_tool.py b/mcp_cloud/tests/test_task_retry_tool.py index a0b7da3b7..be7c35066 100644 --- a/mcp_cloud/tests/test_task_retry_tool.py +++ b/mcp_cloud/tests/test_task_retry_tool.py @@ -4,14 +4,14 @@ from unittest.mock import patch from mcp.types import CallToolResult -from mcp_cloud.app import handle_list_tools, handle_task_retry +from mcp_cloud.app import handle_list_tools, handle_plan_retry as handle_task_retry class TestTaskRetryTool(unittest.TestCase): def test_task_retry_tool_listed(self): tools = asyncio.run(handle_list_tools()) tool_names = {tool.name for tool in tools} - self.assertIn("task_retry", tool_names) + self.assertIn("plan_retry", tool_names) def test_task_retry_returns_structured_content(self): task_id = str(uuid.uuid4()) diff --git a/mcp_cloud/tests/test_task_status_tool.py b/mcp_cloud/tests/test_task_status_tool.py index d48309ca6..30d514ddb 100644 --- a/mcp_cloud/tests/test_task_status_tool.py +++ b/mcp_cloud/tests/test_task_status_tool.py @@ -6,7 +6,7 @@ from mcp.types import CallToolResult from database_api.model_taskitem import TaskState -from mcp_cloud.app import handle_task_status +from mcp_cloud.app import handle_plan_status as handle_task_status class TestTaskStatusTool(unittest.TestCase): diff --git a/mcp_cloud/tests/test_tool_surface_consistency.py b/mcp_cloud/tests/test_tool_surface_consistency.py index b773659fb..9adbf8660 100644 --- a/mcp_cloud/tests/test_tool_surface_consistency.py +++ b/mcp_cloud/tests/test_tool_surface_consistency.py @@ -38,44 +38,44 @@ def test_local_all_tools_have_output_schema(self): ) -class TestTaskCreateInputSchemaHasUserApiKey(unittest.TestCase): - """user_api_key must be in the visible task_create input schema.""" +class TestPlanCreateInputSchemaHasUserApiKey(unittest.TestCase): + """user_api_key must be in the visible plan_create input schema.""" - def test_cloud_task_create_schema_has_user_api_key(self): - props = cloud_app.TASK_CREATE_INPUT_SCHEMA.get("properties", {}) + def test_cloud_plan_create_schema_has_user_api_key(self): + props = cloud_app.PLAN_CREATE_INPUT_SCHEMA.get("properties", {}) self.assertIn("user_api_key", props) - def test_local_task_create_schema_has_user_api_key(self): - props = local_app.TASK_CREATE_INPUT_SCHEMA.get("properties", {}) + def test_local_plan_create_schema_has_user_api_key(self): + props = local_app.PLAN_CREATE_INPUT_SCHEMA.get("properties", {}) self.assertIn("user_api_key", props) -class TestTaskListInputSchemaHasUserApiKey(unittest.TestCase): - """user_api_key must be required in the task_list input schema.""" +class TestPlanListInputSchemaHasUserApiKey(unittest.TestCase): + """user_api_key must be required in the plan_list input schema.""" - def test_cloud_task_list_schema_requires_user_api_key(self): - props = cloud_app.TASK_LIST_INPUT_SCHEMA.get("properties", {}) + def test_cloud_plan_list_schema_requires_user_api_key(self): + props = cloud_app.PLAN_LIST_INPUT_SCHEMA.get("properties", {}) self.assertIn("user_api_key", props) - required = cloud_app.TASK_LIST_INPUT_SCHEMA.get("required", []) + required = cloud_app.PLAN_LIST_INPUT_SCHEMA.get("required", []) self.assertIn("user_api_key", required) - def test_local_task_list_schema_requires_user_api_key(self): - props = local_app.TASK_LIST_INPUT_SCHEMA.get("properties", {}) + def test_local_plan_list_schema_requires_user_api_key(self): + props = local_app.PLAN_LIST_INPUT_SCHEMA.get("properties", {}) self.assertIn("user_api_key", props) - required = local_app.TASK_LIST_INPUT_SCHEMA.get("required", []) + required = local_app.PLAN_LIST_INPUT_SCHEMA.get("required", []) self.assertIn("user_api_key", required) -class TestTaskRetryInputSchemaDefaults(unittest.TestCase): - """task_retry should default model_profile to baseline.""" +class TestPlanRetryInputSchemaDefaults(unittest.TestCase): + """plan_retry should default model_profile to baseline.""" - def test_cloud_task_retry_schema_defaults_model_profile(self): - props = cloud_app.TASK_RETRY_INPUT_SCHEMA.get("properties", {}) + def test_cloud_plan_retry_schema_defaults_model_profile(self): + props = cloud_app.PLAN_RETRY_INPUT_SCHEMA.get("properties", {}) model_profile = props.get("model_profile", {}) self.assertEqual(model_profile.get("default"), "baseline") - def test_local_task_retry_schema_defaults_model_profile(self): - props = local_app.TASK_RETRY_INPUT_SCHEMA.get("properties", {}) + def test_local_plan_retry_schema_defaults_model_profile(self): + props = local_app.PLAN_RETRY_INPUT_SCHEMA.get("properties", {}) model_profile = props.get("model_profile", {}) self.assertEqual(model_profile.get("default"), "baseline") @@ -119,12 +119,12 @@ def test_local_model_profiles_annotations(self): class TestRemainingToolAnnotations(unittest.TestCase): def test_cloud_remaining_tool_annotations(self): expected = { - "task_create": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, - "task_status": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, - "task_stop": {"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True, "openWorldHint": False}, - "task_retry": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, - "task_file_info": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, - "task_list": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, + "plan_create": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, + "plan_status": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, + "plan_stop": {"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True, "openWorldHint": False}, + "plan_retry": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, + "plan_file_info": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, + "plan_list": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, } for tool_name, expected_annotations in expected.items(): with self.subTest(tool=tool_name): @@ -133,12 +133,12 @@ def test_cloud_remaining_tool_annotations(self): def test_local_remaining_tool_annotations(self): expected = { - "task_create": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, - "task_status": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, - "task_stop": {"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True, "openWorldHint": False}, - "task_retry": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, - "task_download": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, - "task_list": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, + "plan_create": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, + "plan_status": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, + "plan_stop": {"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True, "openWorldHint": False}, + "plan_retry": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, + "plan_download": {"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, + "plan_list": {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, } for tool_name, expected_annotations in expected.items(): with self.subTest(tool=tool_name): @@ -151,29 +151,29 @@ def test_cloud_exposes_model_profiles_tool(self): cloud_tool_names = {definition.name for definition in cloud_app.TOOL_DEFINITIONS} self.assertIn("model_profiles", cloud_tool_names) - def test_cloud_exposes_task_retry_tool(self): + def test_cloud_exposes_plan_retry_tool(self): cloud_tool_names = {definition.name for definition in cloud_app.TOOL_DEFINITIONS} - self.assertIn("task_retry", cloud_tool_names) + self.assertIn("plan_retry", cloud_tool_names) - def test_cloud_exposes_task_file_info_not_task_download(self): + def test_cloud_exposes_plan_file_info_not_plan_download(self): cloud_tool_names = {definition.name for definition in cloud_app.TOOL_DEFINITIONS} - self.assertIn("task_file_info", cloud_tool_names) - self.assertNotIn("task_download", cloud_tool_names) + self.assertIn("plan_file_info", cloud_tool_names) + self.assertNotIn("plan_download", cloud_tool_names) - def test_cloud_exposes_task_list_tool(self): + def test_cloud_exposes_plan_list_tool(self): cloud_tool_names = {definition.name for definition in cloud_app.TOOL_DEFINITIONS} - self.assertIn("task_list", cloud_tool_names) + self.assertIn("plan_list", cloud_tool_names) def test_cloud_instructions_reference_cloud_download_tool(self): - self.assertIn("task_file_info", cloud_app.PLANEXE_SERVER_INSTRUCTIONS) - self.assertNotIn("task_download", cloud_app.PLANEXE_SERVER_INSTRUCTIONS) + self.assertIn("plan_file_info", cloud_app.PLANEXE_SERVER_INSTRUCTIONS) + self.assertNotIn("plan_download", cloud_app.PLANEXE_SERVER_INSTRUCTIONS) - def test_cloud_task_create_description_references_cloud_download_tool(self): - description = _tool_desc(cloud_app.TOOL_DEFINITIONS, "task_create") - self.assertIn("task_file_info", description) - self.assertNotIn("task_download", description) + def test_cloud_plan_create_description_references_cloud_download_tool(self): + description = _tool_desc(cloud_app.TOOL_DEFINITIONS, "plan_create") + self.assertIn("plan_file_info", description) + self.assertNotIn("plan_download", description) - def test_cloud_instructions_include_task_status_state_contract(self): + def test_cloud_instructions_include_plan_status_state_contract(self): instructions = cloud_app.PLANEXE_SERVER_INSTRUCTIONS self.assertIn("pending/processing", instructions) self.assertIn("completed", instructions) @@ -183,8 +183,8 @@ def test_cloud_instructions_include_task_status_state_contract(self): self.assertIn("longer than 20 minutes", instructions) self.assertIn("PlanExeOrg/PlanExe/issues", instructions) - def test_cloud_task_status_description_includes_state_contract(self): - description = _tool_desc(cloud_app.TOOL_DEFINITIONS, "task_status") + def test_cloud_plan_status_description_includes_state_contract(self): + description = _tool_desc(cloud_app.TOOL_DEFINITIONS, "plan_status") self.assertIn("pending/processing", description) self.assertIn("completed", description) self.assertIn("failed", description) @@ -198,7 +198,7 @@ def test_cloud_instructions_include_model_profiles_unavailable_guidance(self): self.assertIn("MODEL_PROFILES_UNAVAILABLE", instructions) def test_cloud_prompt_schema_includes_prompt_shape_guidance(self): - prompt_schema = cloud_app.TASK_CREATE_INPUT_SCHEMA["properties"]["prompt"]["description"] + prompt_schema = cloud_app.PLAN_CREATE_INPUT_SCHEMA["properties"]["prompt"]["description"] self.assertIn("300-800 words", prompt_schema) self.assertIn("objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria", prompt_schema) @@ -208,29 +208,29 @@ def test_local_exposes_model_profiles_tool(self): local_tool_names = {definition.name for definition in local_app.TOOL_DEFINITIONS} self.assertIn("model_profiles", local_tool_names) - def test_local_exposes_task_retry_tool(self): + def test_local_exposes_plan_retry_tool(self): local_tool_names = {definition.name for definition in local_app.TOOL_DEFINITIONS} - self.assertIn("task_retry", local_tool_names) + self.assertIn("plan_retry", local_tool_names) - def test_local_exposes_task_download_not_task_file_info(self): + def test_local_exposes_plan_download_not_plan_file_info(self): local_tool_names = {definition.name for definition in local_app.TOOL_DEFINITIONS} - self.assertIn("task_download", local_tool_names) - self.assertNotIn("task_file_info", local_tool_names) + self.assertIn("plan_download", local_tool_names) + self.assertNotIn("plan_file_info", local_tool_names) - def test_local_exposes_task_list_tool(self): + def test_local_exposes_plan_list_tool(self): local_tool_names = {definition.name for definition in local_app.TOOL_DEFINITIONS} - self.assertIn("task_list", local_tool_names) + self.assertIn("plan_list", local_tool_names) def test_local_instructions_reference_local_download_tool(self): - self.assertIn("task_download", local_app.PLANEXE_SERVER_INSTRUCTIONS) - self.assertNotIn("task_file_info", local_app.PLANEXE_SERVER_INSTRUCTIONS) + self.assertIn("plan_download", local_app.PLANEXE_SERVER_INSTRUCTIONS) + self.assertNotIn("plan_file_info", local_app.PLANEXE_SERVER_INSTRUCTIONS) - def test_local_task_create_description_references_local_download_tool(self): - description = _tool_desc(local_app.TOOL_DEFINITIONS, "task_create") - self.assertIn("task_download", description) - self.assertNotIn("task_file_info", description) + def test_local_plan_create_description_references_local_download_tool(self): + description = _tool_desc(local_app.TOOL_DEFINITIONS, "plan_create") + self.assertIn("plan_download", description) + self.assertNotIn("plan_file_info", description) - def test_local_instructions_include_task_status_state_contract(self): + def test_local_instructions_include_plan_status_state_contract(self): instructions = local_app.PLANEXE_SERVER_INSTRUCTIONS self.assertIn("pending/processing", instructions) self.assertIn("completed", instructions) @@ -240,8 +240,8 @@ def test_local_instructions_include_task_status_state_contract(self): self.assertIn("longer than 20 minutes", instructions) self.assertIn("PlanExeOrg/PlanExe/issues", instructions) - def test_local_task_status_description_includes_state_contract(self): - description = _tool_desc(local_app.TOOL_DEFINITIONS, "task_status") + def test_local_plan_status_description_includes_state_contract(self): + description = _tool_desc(local_app.TOOL_DEFINITIONS, "plan_status") self.assertIn("pending/processing", description) self.assertIn("completed", description) self.assertIn("failed", description) diff --git a/mcp_cloud/tool_models.py b/mcp_cloud/tool_models.py index bfcffc52b..4925f0360 100644 --- a/mcp_cloud/tool_models.py +++ b/mcp_cloud/tool_models.py @@ -14,7 +14,7 @@ class PromptExamplesOutput(BaseModel): ..., description=( "Example prompts that define the baseline for what a good prompt looks like. " - "Take inspiration from these when writing your own prompt for task_create " + "Take inspiration from these when writing your own prompt for plan_create " "(typically ~300-800 words). Good prompt shape: objective, scope, constraints, " "timeline, stakeholders, budget/resources, and success criteria." ), @@ -48,7 +48,7 @@ class ModelProfileModelEntry(BaseModel): class ModelProfileInfo(BaseModel): profile: Literal["baseline", "premium", "frontier", "custom"] = Field( ..., - description="Model profile value accepted by task_create.model_profile.", + description="Model profile value accepted by plan_create.model_profile.", ) title: str = Field(..., description="Human-friendly profile label.") summary: str = Field(..., description="Short profile guidance for callers.") @@ -62,30 +62,30 @@ class ModelProfileInfo(BaseModel): class ModelProfilesOutput(BaseModel): default_profile: Literal["baseline", "premium", "frontier", "custom"] = Field( ..., - description="Default model profile used when task_create.model_profile is omitted/invalid.", + description="Default model profile used when plan_create.model_profile is omitted/invalid.", ) profiles: list[ModelProfileInfo] = Field( ..., description="Available profile options and their model inventory.", ) - message: str = Field(..., description="Caller guidance for selecting task_create.model_profile.") + message: str = Field(..., description="Caller guidance for selecting plan_create.model_profile.") -class TaskStatusInput(BaseModel): +class PlanStatusInput(BaseModel): task_id: str = Field( ..., - description="Task UUID returned by task_create. Use it to reference the plan being created.", + description="Task UUID returned by plan_create. Use it to reference the plan being created.", ) -class TaskStopInput(BaseModel): +class PlanStopInput(BaseModel): task_id: str = Field( ..., - description="The UUID returned by task_create. Call task_stop with this task_id to request the plan generation to stop.", + description="The UUID returned by plan_create. Call plan_stop with this task_id to request the plan generation to stop.", ) -class TaskRetryInput(BaseModel): +class PlanRetryInput(BaseModel): task_id: str = Field( ..., description="UUID of the failed task to retry.", @@ -98,10 +98,10 @@ class TaskRetryInput(BaseModel): ) -class TaskFileInfoInput(BaseModel): +class PlanFileInfoInput(BaseModel): task_id: str = Field( ..., - description="Task UUID returned by task_create. Use it to download the created plan.", + description="Task UUID returned by plan_create. Use it to download the created plan.", ) artifact: str = Field( default="report", @@ -109,28 +109,28 @@ class TaskFileInfoInput(BaseModel): ) -class TaskCreateOutput(BaseModel): +class PlanCreateOutput(BaseModel): task_id: str = Field( ..., - description="Task UUID returned by task_create. Stable across task_status/task_stop/task_file_info." + description="Task UUID returned by plan_create. Stable across plan_status/plan_stop/plan_file_info." ) created_at: str -class TaskStatusTiming(BaseModel): +class PlanStatusTiming(BaseModel): started_at: str | None elapsed_sec: float -class TaskStatusFile(BaseModel): +class PlanStatusFile(BaseModel): path: str updated_at: str -class TaskStatusSuccess(BaseModel): +class PlanStatusSuccess(BaseModel): task_id: str = Field( ..., - description="Task UUID returned by task_create." + description="Task UUID returned by plan_create." ) state: Literal["pending", "processing", "completed", "failed"] = Field( ..., @@ -143,8 +143,8 @@ class TaskStatusSuccess(BaseModel): ..., description="Completion progress from 0 to 100. Monotonically increasing; 100 when state is completed.", ) - timing: TaskStatusTiming - files: list[TaskStatusFile] = Field( + timing: PlanStatusTiming + files: list[PlanStatusFile] = Field( ..., description=( "Intermediate output files produced so far. " @@ -154,10 +154,10 @@ class TaskStatusSuccess(BaseModel): ) -class TaskStatusOutput(BaseModel): +class PlanStatusOutput(BaseModel): task_id: str | None = Field( default=None, - description="Task UUID returned by task_create." + description="Task UUID returned by plan_create." ) state: Literal["pending", "processing", "completed", "failed"] | None = Field( default=None, @@ -170,8 +170,8 @@ class TaskStatusOutput(BaseModel): default=None, description="Completion progress from 0 to 100. Monotonically increasing; 100 when state is completed.", ) - timing: TaskStatusTiming | None = None - files: list[TaskStatusFile] | None = Field( + timing: PlanStatusTiming | None = None + files: list[PlanStatusFile] | None = Field( default=None, description=( "Intermediate output files produced so far. " @@ -182,7 +182,7 @@ class TaskStatusOutput(BaseModel): error: ErrorDetail | None = None -class TaskStopOutput(BaseModel): +class PlanStopOutput(BaseModel): state: Literal["pending", "processing", "completed", "failed"] | None = Field( default=None, description="Current task state after stop request.", @@ -194,7 +194,7 @@ class TaskStopOutput(BaseModel): error: ErrorDetail | None = None -class TaskRetryOutput(BaseModel): +class PlanRetryOutput(BaseModel): task_id: str | None = Field( default=None, description="Task UUID that was retried (same ID as the failed task).", @@ -214,12 +214,12 @@ class TaskRetryOutput(BaseModel): error: ErrorDetail | None = None -class TaskFileInfoNotReadyOutput(BaseModel): +class PlanFileInfoNotReadyOutput(BaseModel): ready: bool = Field(False, description="Always False; indicates the artifact is not yet available.") reason: str = Field(..., description="Human-readable explanation, e.g. 'processing' or 'failed'.") -class TaskFileInfoReadyOutput(BaseModel): +class PlanFileInfoReadyOutput(BaseModel): content_type: str = Field(..., description="Artifact content type.") sha256: str = Field(..., description="SHA-256 hash of artifact bytes.") download_size: int = Field(..., description="Artifact size in bytes.") @@ -229,7 +229,7 @@ class TaskFileInfoReadyOutput(BaseModel): ) -class TaskFileInfoOutput(BaseModel): +class PlanFileInfoOutput(BaseModel): content_type: str | None = Field(default=None, description="Artifact content type.") sha256: str | None = Field(default=None, description="SHA-256 hash of artifact bytes.") download_size: int | None = Field(default=None, description="Artifact size in bytes.") @@ -240,7 +240,7 @@ class TaskFileInfoOutput(BaseModel): error: ErrorDetail | None = None -class TaskListInput(BaseModel): +class PlanListInput(BaseModel): user_api_key: str = Field( ..., description="User API key (pex_...) to scope the task list to the authenticated user.", @@ -253,7 +253,7 @@ class TaskListInput(BaseModel): ) -class TaskListItem(BaseModel): +class PlanListItem(BaseModel): task_id: str = Field(..., description="Task UUID.") state: Literal["pending", "processing", "completed", "failed"] = Field( ..., @@ -264,24 +264,24 @@ class TaskListItem(BaseModel): prompt_excerpt: str = Field(..., description="First 100 characters of the prompt.") -class TaskListOutput(BaseModel): - tasks: list[TaskListItem] = Field(..., description="Tasks for the authenticated user, newest first.") +class PlanListOutput(BaseModel): + tasks: list[PlanListItem] = Field(..., description="Tasks for the authenticated user, newest first.") message: str = Field(..., description="Human-readable summary (e.g. how many tasks were returned).") -class TaskCreateInput(BaseModel): +class PlanCreateInput(BaseModel): prompt: str = Field( ..., description=( "What the plan should cover (goal, context, constraints). " - "Use prompt_examples to get example prompts; use these as examples for task_create. " + "Use prompt_examples to get example prompts; use these as examples for plan_create. " "For best results, provide a detailed prompt (typically ~300-800 words). " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, " "budget/resources, and success criteria. " "Write as flowing prose, not structured markdown. Include banned approaches, " "governance preferences, and phasing inline. " "Short prompts produce less detailed plans. " - "Do not use task_create for tiny one-shot outputs (e.g., a 5-point checklist); use direct LLM responses for those." + "Do not use plan_create for tiny one-shot outputs (e.g., a 5-point checklist); use direct LLM responses for those." ), ) model_profile: Literal["baseline", "premium", "frontier", "custom"] = Field( @@ -295,3 +295,26 @@ class TaskCreateInput(BaseModel): default=None, description="Optional user API key for credits and attribution.", ) + + +# --------------------------------------------------------------------------- +# Backward-compatible aliases for old Task* names (used internally in app.py) +# --------------------------------------------------------------------------- +TaskCreateInput = PlanCreateInput +TaskCreateOutput = PlanCreateOutput +TaskStatusInput = PlanStatusInput +TaskStatusOutput = PlanStatusOutput +TaskStatusTiming = PlanStatusTiming +TaskStatusFile = PlanStatusFile +TaskStatusSuccess = PlanStatusSuccess +TaskStopInput = PlanStopInput +TaskStopOutput = PlanStopOutput +TaskRetryInput = PlanRetryInput +TaskRetryOutput = PlanRetryOutput +TaskFileInfoInput = PlanFileInfoInput +TaskFileInfoOutput = PlanFileInfoOutput +TaskFileInfoNotReadyOutput = PlanFileInfoNotReadyOutput +TaskFileInfoReadyOutput = PlanFileInfoReadyOutput +TaskListInput = PlanListInput +TaskListItem = PlanListItem +TaskListOutput = PlanListOutput diff --git a/mcp_local/AGENTS.md b/mcp_local/AGENTS.md index f260f9c02..b80d7ec63 100644 --- a/mcp_local/AGENTS.md +++ b/mcp_local/AGENTS.md @@ -6,16 +6,16 @@ to mcp_cloud, a MCP server running in the cloud, over HTTP. ## Interaction model - The local proxy exposes MCP tools over stdio and forwards requests to mcp_cloud using `PLANEXE_URL` (defaults to the hosted `/mcp` endpoint). -- Supported tools: `prompt_examples`, `model_profiles`, `task_create`, `task_status`, `task_stop`, `task_download`. -- `task_download` calls the remote `task_file_info` tool to obtain a download URL, +- Supported tools: `prompt_examples`, `model_profiles`, `plan_create`, `plan_status`, `plan_stop`, `plan_download`. +- `plan_download` calls the remote `plan_file_info` tool to obtain a download URL, then downloads the artifact to `PLANEXE_PATH` on the local machine. -- `task_create` visible input schema includes `prompt`, optional `model_profile`, and optional `user_api_key`. -- Use `model_profiles` to help agents select `task_create.model_profile` without relying on internal file knowledge. -- Keep workflow wording explicit that prompt drafting + user approval is a non-tool step before `task_create`. -- Keep concurrency wording explicit: each `task_create` call creates a new `task_id`; no global per-client concurrency cap is enforced server-side. +- `plan_create` visible input schema includes `prompt`, optional `model_profile`, and optional `user_api_key`. +- Use `model_profiles` to help agents select `plan_create.model_profile` without relying on internal file knowledge. +- Keep workflow wording explicit that prompt drafting + user approval is a non-tool step before `plan_create`. +- Keep concurrency wording explicit: each `plan_create` call creates a new `task_id`; no global per-client concurrency cap is enforced server-side. ## Public state contract -- `task_status.state` must use exactly: `pending`, `processing`, `completed`, `failed`. +- `plan_status.state` must use exactly: `pending`, `processing`, `completed`, `failed`. - Caller contract: - `pending`/`processing`: keep polling. - `completed`: download is ready. @@ -28,8 +28,8 @@ to mcp_cloud, a MCP server running in the cloud, over HTTP. - `processing` with no output-file changes for longer than 20 minutes likely means stalled/failed execution. - Report both cases at `https://github.com/PlanExeOrg/PlanExe/issues`. -## task_stop semantics -- `task_stop` is a stop request/acknowledgement, not a separate lifecycle state. +## plan_stop semantics +- `plan_stop` is a stop request/acknowledgement, not a separate lifecycle state. - Return payload should include current public `state` plus `stop_requested`. ## Constraints @@ -40,10 +40,10 @@ to mcp_cloud, a MCP server running in the cloud, over HTTP. - Ensure all tool responses include structured content when an output schema is defined. - Keep local proxy error semantics documented and stable (`REMOTE_ERROR`, `DOWNLOAD_FAILED`) and pass through cloud error payloads unchanged when possible. - Tool-surface split must remain explicit: - - local exposes `task_download`. - - cloud exposes `task_file_info`. - - do not expose `task_file_info` as a local tool name. -- **Run as task**: Do not advertise the MCP **tasks** protocol (tasks/get, tasks/result, tasks/cancel, tasks/list) or add tool-level "Run as task" support. PlanExe’s interface is tool-based only (task_create → task_status → task_download). The MCP tasks protocol is a different, client-driven feature; Cursor and the Python MCP SDK do not support it properly, so we keep tools-only for compatibility. + - local exposes `plan_download`. + - cloud exposes `plan_file_info`. + - do not expose `plan_file_info` as a local tool name. +- **Run as task**: Do not advertise the MCP **tasks** protocol (tasks/get, tasks/result, tasks/cancel, tasks/list) or add tool-level "Run as task" support. PlanExe’s interface is tool-based only (plan_create → plan_status → plan_download). The MCP tasks protocol is a different, client-driven feature; Cursor and the Python MCP SDK do not support it properly, so we keep tools-only for compatibility. ## Env vars - `PLANEXE_URL`: Base URL for mcp_cloud (e.g., `http://localhost:8001/mcp`). diff --git a/mcp_local/README.md b/mcp_local/README.md index 46dce1481..c082b983e 100644 --- a/mcp_local/README.md +++ b/mcp_local/README.md @@ -8,36 +8,36 @@ proxy forwards tool calls over HTTP and downloads artifacts from `/download/{tas ## Tools -`prompt_examples` - Return example prompts. Use these as examples for task_create. You can also call `task_create` with any prompt—short prompts produce less detailed plans. +`prompt_examples` - Return example prompts. Use these as examples for plan_create. You can also call `plan_create` with any prompt—short prompts produce less detailed plans. `model_profiles` - Show model_profile options and currently available models in each profile. -`task_create` - Initiate creation of a plan. -`task_status` - Get status and progress about the creation of a plan. -`task_stop` - Abort creation of a plan. -`task_retry` - Retry a failed task using the same task id (optional model_profile, defaults to baseline). -`task_download` - Download the plan, either html report or a zip with everything, and save it to disk. +`plan_create` - Initiate creation of a plan. +`plan_status` - Get status and progress about the creation of a plan. +`plan_stop` - Abort creation of a plan. +`plan_retry` - Retry a failed task using the same task id (optional model_profile, defaults to baseline). +`plan_download` - Download the plan, either html report or a zip with everything, and save it to disk. -`task_status` caller contract: +`plan_status` caller contract: - `pending` / `processing`: keep polling. - `completed`: terminal success, download is ready. - `failed`: terminal error. Concurrency semantics: -- Each `task_create` call creates a new `task_id`. -- `task_retry` reuses the same failed `task_id`. +- Each `plan_create` call creates a new `task_id`. +- `plan_retry` reuses the same failed `task_id`. - Server does not enforce a global one-task-at-a-time cap per client. - Local clients should track task ids explicitly when running tasks in parallel. Minimal error contract: - Tool errors use `{"error":{"code","message","details?"}}`. - Common proxied cloud codes include: `TASK_NOT_FOUND`, `INVALID_USER_API_KEY`, `USER_API_KEY_REQUIRED`, `INSUFFICIENT_CREDITS`, `INTERNAL_ERROR`, `generation_failed`, `content_unavailable`. -- `task_retry` may return `TASK_NOT_FAILED` if the task is not currently failed. +- `plan_retry` may return `TASK_NOT_FAILED` if the task is not currently failed. - Local proxy specific codes: `REMOTE_ERROR`, `DOWNLOAD_FAILED`. -- `task_file_info` (called under the hood by task_download) may return `{}` while output is not ready. +- `plan_file_info` (called under the hood by plan_download) may return `{}` while output is not ready. -**Tip**: Call `prompt_examples` to get example prompts to use with task_create. The full catalog lives at `worker_plan/worker_plan_api/prompt/data/simple_plan_prompts.jsonl`. +**Tip**: Call `prompt_examples` to get example prompts to use with plan_create. The full catalog lives at `worker_plan/worker_plan_api/prompt/data/simple_plan_prompts.jsonl`. -`task_download` is a synthetic tool provided by the local proxy. It calls the -remote MCP tool `task_file_info` to obtain a download URL, then downloads the +`plan_download` is a synthetic tool provided by the local proxy. It calls the +remote MCP tool `plan_file_info` to obtain a download URL, then downloads the file locally into `PLANEXE_PATH`. `PLANEXE_PATH` behavior: @@ -45,13 +45,13 @@ file locally into `PLANEXE_PATH`. - If the path does not exist, it is created. - If the path points to a file (not a directory), download fails. - Filenames are `-030-report.html` or `-run.zip` (with `-1`, `-2`, ... suffixes on collisions). -- `task_download` returns `saved_path` with the final file location. +- `plan_download` returns `saved_path` with the final file location. ## Run as task (MCP tasks protocol) Some MCP clients (e.g. the MCP Inspector) show a **"Run as task"** option for tools. That refers to the MCP **tasks** protocol: a separate mechanism where the client runs a tool in the background using RPC methods like `tasks/run`, `tasks/get`, `tasks/result`, and `tasks/cancel`, instead of a single blocking tool call. -**PlanExe does not use or advertise the MCP tasks protocol.** Our interface is **tool-based** only: the agent calls `prompt_examples` and `model_profiles` for setup, completes a non-tool prompt drafting/approval step, then `task_create` → gets a `task_id` → polls `task_status` → optionally calls `task_retry` if failed → uses `task_download`. That flow is defined in `docs/mcp/planexe_mcp_interface.md` and is the intended design. +**PlanExe does not use or advertise the MCP tasks protocol.** Our interface is **tool-based** only: the agent calls `prompt_examples` and `model_profiles` for setup, completes a non-tool prompt drafting/approval step, then `plan_create` → gets a `task_id` → polls `plan_status` → optionally calls `plan_retry` if failed → uses `plan_download`. That flow is defined in `docs/mcp/planexe_mcp_interface.md` and is the intended design. You should **not** enable "Run as task" for PlanExe. The Python MCP SDK and clients like Cursor do not properly support the tasks protocol (method registration and initialization fail). Use the tools directly: create a task, poll status, then download when done. diff --git a/mcp_local/planexe_mcp_local.py b/mcp_local/planexe_mcp_local.py index d44e3baae..f23db46f2 100644 --- a/mcp_local/planexe_mcp_local.py +++ b/mcp_local/planexe_mcp_local.py @@ -328,20 +328,20 @@ class ToolDefinition: "required": ["code", "message"], } -TASK_CREATE_INPUT_SCHEMA = { +PLAN_CREATE_INPUT_SCHEMA = { "type": "object", "properties": { "prompt": { "type": "string", "description": ( "What the plan should cover. Good prompts are often 300–800 words. " - "Use prompt_examples to get example prompts; use these as examples for task_create. " + "Use prompt_examples to get example prompts; use these as examples for plan_create. " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, " "budget/resources, and success criteria. " "Write as flowing prose, not structured markdown. Include banned approaches, " "governance preferences, and phasing inline. " "Short prompts produce less detailed plans. " - "Do not use task_create for tiny one-shot outputs (e.g., a 5-point checklist)." + "Do not use plan_create for tiny one-shot outputs (e.g., a 5-point checklist)." ), }, "model_profile": { @@ -362,29 +362,29 @@ class ToolDefinition: "required": ["prompt"], } -TASK_STATUS_INPUT_SCHEMA = { +PLAN_STATUS_INPUT_SCHEMA = { "type": "object", "properties": { "task_id": { "type": "string", - "description": "UUID of the task (returned by task_create).", + "description": "UUID of the task (returned by plan_create).", }, }, "required": ["task_id"], } -TASK_STOP_INPUT_SCHEMA = { +PLAN_STOP_INPUT_SCHEMA = { "type": "object", "properties": { "task_id": { "type": "string", - "description": "UUID of the task to stop (returned by task_create).", + "description": "UUID of the task to stop (returned by plan_create).", }, }, "required": ["task_id"], } -TASK_RETRY_INPUT_SCHEMA = { +PLAN_RETRY_INPUT_SCHEMA = { "type": "object", "properties": { "task_id": { @@ -401,12 +401,12 @@ class ToolDefinition: "required": ["task_id"], } -TASK_DOWNLOAD_INPUT_SCHEMA = { +PLAN_DOWNLOAD_INPUT_SCHEMA = { "type": "object", "properties": { "task_id": { "type": "string", - "description": "UUID of the task (returned by task_create).", + "description": "UUID of the task (returned by plan_create).", }, "artifact": { "type": "string", @@ -418,6 +418,13 @@ class ToolDefinition: "required": ["task_id"], } +# Backward-compatible aliases +TASK_CREATE_INPUT_SCHEMA = PLAN_CREATE_INPUT_SCHEMA +TASK_STATUS_INPUT_SCHEMA = PLAN_STATUS_INPUT_SCHEMA +TASK_STOP_INPUT_SCHEMA = PLAN_STOP_INPUT_SCHEMA +TASK_RETRY_INPUT_SCHEMA = PLAN_RETRY_INPUT_SCHEMA +TASK_DOWNLOAD_INPUT_SCHEMA = PLAN_DOWNLOAD_INPUT_SCHEMA + PROMPT_EXAMPLES_INPUT_SCHEMA = { "type": "object", "properties": {}, @@ -491,7 +498,7 @@ class ToolDefinition: ], } -TASK_CREATE_OUTPUT_SCHEMA = { +PLAN_CREATE_OUTPUT_SCHEMA = { "type": "object", "properties": { "task_id": {"type": "string"}, @@ -500,7 +507,7 @@ class ToolDefinition: "required": ["task_id", "created_at"], } -TASK_STATUS_OUTPUT_SCHEMA = { +PLAN_STATUS_OUTPUT_SCHEMA = { "type": "object", "properties": { "task_id": {"type": ["string", "null"]}, @@ -526,7 +533,7 @@ class ToolDefinition: }, } -TASK_STOP_OUTPUT_SCHEMA = { +PLAN_STOP_OUTPUT_SCHEMA = { "type": "object", "properties": { "state": {"type": "string"}, @@ -535,7 +542,7 @@ class ToolDefinition: }, } -TASK_RETRY_OUTPUT_SCHEMA = { +PLAN_RETRY_OUTPUT_SCHEMA = { "type": "object", "properties": { "task_id": {"type": "string"}, @@ -549,7 +556,7 @@ class ToolDefinition: }, } -TASK_DOWNLOAD_OUTPUT_SCHEMA = { +PLAN_DOWNLOAD_OUTPUT_SCHEMA = { "type": "object", "properties": { "content_type": {"type": "string", "description": "Artifact content type."}, @@ -558,14 +565,14 @@ class ToolDefinition: "download_url": {"type": "string", "description": "Remote URL used for download."}, "saved_path": { "type": "string", - "description": "Local file path written by task_download.", + "description": "Local file path written by plan_download.", }, "error": ERROR_SCHEMA, }, "additionalProperties": False, } -TASK_LIST_INPUT_SCHEMA = { +PLAN_LIST_INPUT_SCHEMA = { "type": "object", "properties": { "user_api_key": { @@ -582,7 +589,7 @@ class ToolDefinition: }, "required": ["user_api_key"], } -TASK_LIST_OUTPUT_SCHEMA = { +PLAN_LIST_OUTPUT_SCHEMA = { "type": "object", "properties": { "tasks": { @@ -605,18 +612,27 @@ class ToolDefinition: "additionalProperties": False, } +# Backward-compatible aliases +TASK_CREATE_OUTPUT_SCHEMA = PLAN_CREATE_OUTPUT_SCHEMA +TASK_STATUS_OUTPUT_SCHEMA = PLAN_STATUS_OUTPUT_SCHEMA +TASK_STOP_OUTPUT_SCHEMA = PLAN_STOP_OUTPUT_SCHEMA +TASK_RETRY_OUTPUT_SCHEMA = PLAN_RETRY_OUTPUT_SCHEMA +TASK_DOWNLOAD_OUTPUT_SCHEMA = PLAN_DOWNLOAD_OUTPUT_SCHEMA +TASK_LIST_INPUT_SCHEMA = PLAN_LIST_INPUT_SCHEMA +TASK_LIST_OUTPUT_SCHEMA = PLAN_LIST_OUTPUT_SCHEMA + TOOL_DEFINITIONS = [ ToolDefinition( name="prompt_examples", description=( "Call this first. Returns example prompts that define what a good prompt looks like. " - "Do NOT call task_create yet. Optional before task_create: call model_profiles to choose model_profile. " + "Do NOT call plan_create yet. Optional before plan_create: call model_profiles to choose model_profile. " "Next is a non-tool step: formulate a prompt (use examples as a baseline, similar structure) and get user approval. " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. " "Write the prompt as flowing prose, not structured markdown with headers or bullet lists. " "Weave technical specs, constraints, and targets naturally into sentences. Include banned words/approaches and governance preferences inline. " "The examples demonstrate this prose style — match their tone and density. " - "Then call task_create. " + "Then call plan_create. " "PlanExe is not for tiny one-shot outputs like a 5-point checklist; and it does not support selecting only some internal pipeline steps." ), input_schema=PROMPT_EXAMPLES_INPUT_SCHEMA, @@ -631,7 +647,7 @@ class ToolDefinition: ToolDefinition( name="model_profiles", description=( - "Optional helper before task_create. Returns model_profile options with plain-language guidance " + "Optional helper before plan_create. Returns model_profile options with plain-language guidance " "and currently available models in each profile. " "If no models are available, returns error code MODEL_PROFILES_UNAVAILABLE." ), @@ -645,7 +661,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_create", + name="plan_create", description=( "Call only after prompt_examples and after you have completed prompt drafting/approval (non-tool step). " "PlanExe turns the approved prompt into a strategic project-plan draft (20+ sections) in ~10-20 min. " @@ -655,15 +671,15 @@ class ToolDefinition: "plan review (critical issues, KPIs, financial strategy, automation opportunities), Q&A, " "premortem with failure scenarios, self-audit checklist, and adversarial premise attacks that argue against the project. " "The adversarial sections (premortem, self-audit, premise attacks) surface risks and questions the prompter may not have considered. " - "Returns task_id (UUID); use it for task_status, task_stop, task_retry, and task_download. " - "If you lose a task_id, call task_list with your user_api_key to recover it. " - "Each task_create call creates a new task_id (proxied to cloud; no server-side dedup). " + "Returns task_id (UUID); use it for plan_status, plan_stop, plan_retry, and plan_download. " + "If you lose a task_id, call plan_list with your user_api_key to recover it. " + "Each plan_create call creates a new task_id (proxied to cloud; no server-side dedup). " "If you are unsure which model_profile to choose, call model_profiles first. " "If your deployment uses credits, include user_api_key to charge the correct account. " "Common proxied error codes: INVALID_USER_API_KEY, USER_API_KEY_REQUIRED, INSUFFICIENT_CREDITS, REMOTE_ERROR." ), - input_schema=TASK_CREATE_INPUT_SCHEMA, - output_schema=TASK_CREATE_OUTPUT_SCHEMA, + input_schema=PLAN_CREATE_INPUT_SCHEMA, + output_schema=PLAN_CREATE_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": False, @@ -672,7 +688,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_status", + name="plan_status", description=( "Returns status and progress of the plan currently being created. " "Poll at reasonable intervals only (e.g. every 5 minutes): plan generation typically takes 10-20 minutes " @@ -685,8 +701,8 @@ class ToolDefinition: "processing with no file-output changes for >20 minutes likely means failed/stalled. " "Report these issues to https://github.com/PlanExeOrg/PlanExe/issues ." ), - input_schema=TASK_STATUS_INPUT_SCHEMA, - output_schema=TASK_STATUS_OUTPUT_SCHEMA, + input_schema=PLAN_STATUS_INPUT_SCHEMA, + output_schema=PLAN_STATUS_OUTPUT_SCHEMA, annotations={ "readOnlyHint": True, "destructiveHint": False, @@ -695,16 +711,16 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_stop", + name="plan_stop", description=( - "Request the plan generation to stop. Pass the task_id (the UUID returned by task_create). " + "Request the plan generation to stop. Pass the task_id (the UUID returned by plan_create). " "Stopping is asynchronous: the stop flag is set immediately but the task may continue briefly before halting. " "A stopped task will eventually transition to the failed state. " "If the task is already completed or failed, stop_requested returns false (the task already finished). " "Unknown task_id returns TASK_NOT_FOUND (or REMOTE_ERROR when transport fails)." ), - input_schema=TASK_STOP_INPUT_SCHEMA, - output_schema=TASK_STOP_OUTPUT_SCHEMA, + input_schema=PLAN_STOP_INPUT_SCHEMA, + output_schema=PLAN_STOP_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": True, @@ -713,15 +729,15 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_retry", + name="plan_retry", description=( "Retry a task that is currently in failed state. " "Pass the failed task_id and optionally model_profile (defaults to baseline). " "The same task_id is requeued and reset to pending on the cloud service. " "Unknown task_id returns TASK_NOT_FOUND; non-failed tasks return TASK_NOT_FAILED." ), - input_schema=TASK_RETRY_INPUT_SCHEMA, - output_schema=TASK_RETRY_OUTPUT_SCHEMA, + input_schema=PLAN_RETRY_INPUT_SCHEMA, + output_schema=PLAN_RETRY_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": False, @@ -730,7 +746,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_download", + name="plan_download", description=( "Download the plan output and save it locally to PLANEXE_PATH. " "Use artifact='report' (default) for the interactive HTML report (~700KB, self-contained with embedded JS " @@ -740,8 +756,8 @@ class ToolDefinition: "Filename format is - with numeric suffixes when collisions occur. " "Common local error codes: DOWNLOAD_FAILED, REMOTE_ERROR." ), - input_schema=TASK_DOWNLOAD_INPUT_SCHEMA, - output_schema=TASK_DOWNLOAD_OUTPUT_SCHEMA, + input_schema=PLAN_DOWNLOAD_INPUT_SCHEMA, + output_schema=PLAN_DOWNLOAD_OUTPUT_SCHEMA, annotations={ "readOnlyHint": False, "destructiveHint": False, @@ -750,7 +766,7 @@ class ToolDefinition: }, ), ToolDefinition( - name="task_list", + name="plan_list", description=( "List the most recent tasks for an authenticated user. " "Requires user_api_key (pex_...). " @@ -758,8 +774,8 @@ class ToolDefinition: "progress_percentage, created_at (ISO 8601), and a prompt_excerpt (first 100 chars). " "Use this to recover a lost task_id or to review recent activity." ), - input_schema=TASK_LIST_INPUT_SCHEMA, - output_schema=TASK_LIST_OUTPUT_SCHEMA, + input_schema=PLAN_LIST_INPUT_SCHEMA, + output_schema=PLAN_LIST_OUTPUT_SCHEMA, annotations={ "readOnlyHint": True, "destructiveHint": False, @@ -781,23 +797,23 @@ class ToolDefinition: "Do not use PlanExe for tiny one-shot outputs (for example: 'give me a 5-point checklist'); use a normal LLM response for that. " "The planning pipeline is fixed end-to-end; callers cannot select individual internal pipeline steps to run. " "Required interaction order: call prompt_examples first. " - "Optional before task_create: call model_profiles to see profile guidance and available models in each profile. " + "Optional before plan_create: call model_profiles to see profile guidance and available models in each profile. " "Then perform a non-tool step: draft a strong prompt as flowing prose (not structured markdown with headers or bullets), " "typically ~300-800 words, and get user approval. " "Good prompt shape: objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. " "Write the prompt as flowing prose — weave specs, constraints, and targets naturally into sentences. " - "Only after approval, call task_create. " - "Each task_create call creates a new task_id; the server does not enforce a global per-client concurrency limit. " - "Then poll task_status (about every 5 minutes); use task_download when complete. " - "If a run fails, call task_retry with the failed task_id to requeue it (optional model_profile, defaults to baseline). " - "To stop, call task_stop with the task_id from task_create; stopping is asynchronous and the task will eventually transition to failed. " + "Only after approval, call plan_create. " + "Each plan_create call creates a new task_id; the server does not enforce a global per-client concurrency limit. " + "Then poll plan_status (about every 5 minutes); use plan_download when complete. " + "If a run fails, call plan_retry with the failed task_id to requeue it (optional model_profile, defaults to baseline). " + "To stop, call plan_stop with the task_id from plan_create; stopping is asynchronous and the task will eventually transition to failed. " "If model_profiles returns MODEL_PROFILES_UNAVAILABLE, inform the user that no models are currently configured and the server administrator needs to set up model profiles. " - "Tool errors use {error:{code,message}}. task_download may return REMOTE_ERROR or DOWNLOAD_FAILED. " - "task_download saves to PLANEXE_PATH (default: current working directory) and returns saved_path. " - "To list recent tasks for a user call task_list with user_api_key; returns task_id, state, progress_percentage, created_at, and prompt_excerpt. " - "task_status state contract: pending/processing => keep polling; completed => download is ready; failed => terminal error. " - "Troubleshooting: if task_status stays in pending for longer than 5 minutes, the task was likely queued but not picked up by a worker (server issue). " - "If task_status is in processing and output files do not change for longer than 20 minutes, the run likely failed/stalled. " + "Tool errors use {error:{code,message}}. plan_download may return REMOTE_ERROR or DOWNLOAD_FAILED. " + "plan_download saves to PLANEXE_PATH (default: current working directory) and returns saved_path. " + "To list recent tasks for a user call plan_list with user_api_key; returns task_id, state, progress_percentage, created_at, and prompt_excerpt. " + "plan_status state contract: pending/processing => keep polling; completed => download is ready; failed => terminal error. " + "Troubleshooting: if plan_status stays in pending for longer than 5 minutes, the task was likely queued but not picked up by a worker (server issue). " + "If plan_status is in processing and output files do not change for longer than 20 minutes, the run likely failed/stalled. " "In both cases, report the issue to PlanExe developers on GitHub: https://github.com/PlanExeOrg/PlanExe/issues . " "Main output: a self-contained interactive HTML report (~700KB) with collapsible sections and interactive Gantt charts — open in a browser. " "The zip contains the intermediary pipeline files (md, json, csv) that fed the report." @@ -843,7 +859,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> CallToolResu return await handler(arguments) -async def handle_task_create(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_create(arguments: dict[str, Any]) -> CallToolResult: """Create a task in mcp_cloud via the local HTTP proxy. Examples: @@ -870,7 +886,7 @@ async def handle_task_create(arguments: dict[str, Any]) -> CallToolResult: payload["metadata"] = metadata payload, error = _call_remote_tool( - "task_create", + "plan_create", payload, ) if error: @@ -894,14 +910,14 @@ async def handle_model_profiles(arguments: dict[str, Any]) -> CallToolResult: return _wrap_response(payload) -async def handle_task_status(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_status(arguments: dict[str, Any]) -> CallToolResult: """Fetch status/progress for a task from mcp_cloud. Examples: - {"task_id": "uuid"} → state/progress/timing Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. Returns: - content: JSON string matching structuredContent. @@ -909,20 +925,20 @@ async def handle_task_status(arguments: dict[str, Any]) -> CallToolResult: - isError: True when the remote tool call fails. """ req = TaskStatusRequest(**arguments) - payload, error = _call_remote_tool("task_status", {"task_id": req.task_id}) + payload, error = _call_remote_tool("plan_status", {"task_id": req.task_id}) if error: return _wrap_response({"error": error}, is_error=True) return _wrap_response(payload) -async def handle_task_stop(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_stop(arguments: dict[str, Any]) -> CallToolResult: """Request mcp_cloud to stop an active task. Examples: - {"task_id": "uuid"} → stop request acknowledged Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. Returns: - content: JSON string matching structuredContent. @@ -930,17 +946,17 @@ async def handle_task_stop(arguments: dict[str, Any]) -> CallToolResult: - isError: True when the remote tool call fails. """ req = TaskStopRequest(**arguments) - payload, error = _call_remote_tool("task_stop", {"task_id": req.task_id}) + payload, error = _call_remote_tool("plan_stop", {"task_id": req.task_id}) if error: return _wrap_response({"error": error}, is_error=True) return _wrap_response(payload) -async def handle_task_retry(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_retry(arguments: dict[str, Any]) -> CallToolResult: """Request mcp_cloud to retry a failed task.""" req = TaskRetryRequest(**arguments) payload, error = _call_remote_tool( - "task_retry", + "plan_retry", {"task_id": req.task_id, "model_profile": req.model_profile}, ) if error: @@ -948,7 +964,7 @@ async def handle_task_retry(arguments: dict[str, Any]) -> CallToolResult: return _wrap_response(payload) -async def handle_task_download(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_download(arguments: dict[str, Any]) -> CallToolResult: """Download report/zip for a task from mcp_cloud and save it locally. Examples: @@ -956,7 +972,7 @@ async def handle_task_download(arguments: dict[str, Any]) -> CallToolResult: - {"task_id": "uuid", "artifact": "zip"} → download zip Args: - - task_id: Task UUID returned by task_create. + - task_id: Task UUID returned by plan_create. - artifact: Optional "report" or "zip". Returns: @@ -970,7 +986,7 @@ async def handle_task_download(arguments: dict[str, Any]) -> CallToolResult: artifact = "report" payload, error = _call_remote_tool( - "task_file_info", + "plan_file_info", {"task_id": req.task_id, "artifact": artifact}, ) if error: @@ -1014,27 +1030,35 @@ async def handle_task_download(arguments: dict[str, Any]) -> CallToolResult: return _wrap_response(payload) -async def handle_task_list(arguments: dict[str, Any]) -> CallToolResult: +async def handle_plan_list(arguments: dict[str, Any]) -> CallToolResult: """List recent tasks for an authenticated user via mcp_cloud.""" req = TaskListRequest(**arguments) payload_args: dict[str, Any] = {"user_api_key": req.user_api_key, "limit": req.limit} - payload, error = _call_remote_tool("task_list", payload_args) + payload, error = _call_remote_tool("plan_list", payload_args) if error: return _wrap_response({"error": error}, is_error=True) return _wrap_response(payload) TOOL_HANDLERS = { - "task_create": handle_task_create, - "task_status": handle_task_status, - "task_stop": handle_task_stop, - "task_retry": handle_task_retry, - "task_download": handle_task_download, - "task_list": handle_task_list, + "plan_create": handle_plan_create, + "plan_status": handle_plan_status, + "plan_stop": handle_plan_stop, + "plan_retry": handle_plan_retry, + "plan_download": handle_plan_download, + "plan_list": handle_plan_list, "prompt_examples": handle_prompt_examples, "model_profiles": handle_model_profiles, } +# Backward-compatible aliases +handle_task_create = handle_plan_create +handle_task_status = handle_plan_status +handle_task_stop = handle_plan_stop +handle_task_retry = handle_plan_retry +handle_task_download = handle_plan_download +handle_task_list = handle_plan_list + async def main() -> None: logger.info("Starting PlanExe MCP local proxy using %s", _get_mcp_base_url()) diff --git a/public/llms.txt b/public/llms.txt index 87e54ae43..cfb3ee5d3 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -65,25 +65,25 @@ MCP Inspector setup guide: The MCP server exposes tool-based workflows (not MCP tasks protocol): - prompt_examples - model_profiles -- task_create -- task_status -- task_stop -- task_retry -- task_file_info +- plan_create +- plan_status +- plan_stop +- plan_retry +- plan_file_info Key tool inputs/outputs: -- task_create inputs: prompt (required), model_profile (optional: baseline | premium | frontier | custom). -- task_create prompt quality: for best results, provide a detailed prompt as flowing prose (not structured markdown), typically ~300-800 words, with objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. +- plan_create inputs: prompt (required), model_profile (optional: baseline | premium | frontier | custom). +- plan_create prompt quality: for best results, provide a detailed prompt as flowing prose (not structured markdown), typically ~300-800 words, with objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. - model_profiles output: profile guidance + currently available models in each profile. - model_profiles returns `MODEL_PROFILES_UNAVAILABLE` when no models are available in any profile. -- task_create output: task_id (use this for task_status, task_stop, and task_file_info). -- task_status output: current state and progress for a task_id. -- task_retry input: task_id (required), model_profile (optional; defaults to baseline). -- task_retry output: task_id requeued to pending, with model_profile and retried_at. -- task_file_info output: downloadable report/zip metadata and URLs. -- mcp_local task_download output: includes local saved_path where artifact was written. - -task_status caller contract: +- plan_create output: task_id (use this for plan_status, plan_stop, and plan_file_info). +- plan_status output: current state and progress for a task_id. +- plan_retry input: task_id (required), model_profile (optional; defaults to baseline). +- plan_retry output: task_id requeued to pending, with model_profile and retried_at. +- plan_file_info output: downloadable report/zip metadata and URLs. +- mcp_local plan_download output: includes local saved_path where artifact was written. + +plan_status caller contract: - pending: keep polling. - processing: keep polling. - completed: terminal success; download is ready. @@ -93,28 +93,28 @@ Minimal error-handling contract: - Errors use `{"error":{"code","message","details?"}}`. - Common cloud/core codes: `TASK_NOT_FOUND`, `TASK_NOT_FAILED`, `INVALID_USER_API_KEY`, `USER_API_KEY_REQUIRED`, `INSUFFICIENT_CREDITS`, `INTERNAL_ERROR`, `MODEL_PROFILES_UNAVAILABLE`, `generation_failed`, `content_unavailable`. - Common local-proxy codes: `REMOTE_ERROR`, `DOWNLOAD_FAILED`. -- `task_file_info` may return `{}` while artifact output is not ready yet. +- `plan_file_info` may return `{}` while artifact output is not ready yet. Recommended interaction order: 1. Call prompt_examples. 2. Optionally call model_profiles to choose model_profile based on current availability. 3. Non-tool step: prepare and approve a strong prompt as flowing prose (not structured markdown), typically ~300-800 words, with objective, scope, constraints, timeline, stakeholders, budget/resources, and success criteria. -4. Call task_create. -5. Poll task_status until complete (repeat every 5 minutes). -6. If failed, optionally call task_retry (defaults to baseline model_profile). -7. Use task_file_info to get download URLs. -8. Use task_stop if the run must be cancelled. +4. Call plan_create. +5. Poll plan_status until complete (repeat every 5 minutes). +6. If failed, optionally call plan_retry (defaults to baseline model_profile). +7. Use plan_file_info to get download URLs. +8. Use plan_stop if the run must be cancelled. Concurrency semantics: -- Each task_create call creates a new task_id. -- task_retry reuses the same failed task_id. +- Each plan_create call creates a new task_id. +- plan_retry reuses the same failed task_id. - Server does not enforce a global per-client concurrency cap. - Client should track task_ids and usually start with 1 active task, then 2 if needed. Note: -- task_download is provided by mcp_local wrappers in some client setups, not by mcp_cloud directly. +- plan_download is provided by mcp_local wrappers in some client setups, not by mcp_cloud directly. - In mcp_local, downloads save to PLANEXE_PATH (or current working directory if PLANEXE_PATH is unset). -- In mcp_cloud, task_file_info download_url is an absolute URL where the requested artifact can be downloaded. +- In mcp_cloud, plan_file_info download_url is an absolute URL where the requested artifact can be downloaded. ## Authentication @@ -169,7 +169,7 @@ curl -X POST https://mcp.planexe.org/mcp/tools/call \ -H "X-API-Key: " \ -H "Content-Type: application/json" \ -d '{ - "tool": "task_create", + "tool": "plan_create", "arguments": { "prompt": "20-year, €40 billion infrastructure initiative to construct a pillar-supported transoceanic submerged tunnel connecting Spain and Morocco. This project will deploy a system of submerged, buoyant concrete tunnels engineered for high-speed rail traffic, which will be securely anchored at a controlled depth of 100 meters below sea level.", "model_profile": "baseline" diff --git a/skills/planexe-mcp/SKILL.md b/skills/planexe-mcp/SKILL.md index 5337f6f8e..13ea108ae 100644 --- a/skills/planexe-mcp/SKILL.md +++ b/skills/planexe-mcp/SKILL.md @@ -106,7 +106,7 @@ Get example prompts to understand what PlanExe can do. --- -### Tool 2: `task_create` +### Tool 2: `plan_create` Create a new planning task. This is the main entry point for generating plans. @@ -117,7 +117,7 @@ Create a new planning task. This is the main entry point for generating plans. "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_create", + "name": "plan_create", "arguments": { "prompt": "Create a project launch plan for Q2 2026", "model_profile": "premium", @@ -136,7 +136,7 @@ Create a new planning task. This is the main entry point for generating plans. --- -### Tool 3: `task_status` +### Tool 3: `plan_status` Poll the status of a running planning task. @@ -147,7 +147,7 @@ Poll the status of a running planning task. "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_status", + "name": "plan_status", "arguments": { "task_id": "task_abc123def456" } @@ -161,7 +161,7 @@ Poll the status of a running planning task. --- -### Tool 4: `task_stop` +### Tool 4: `plan_stop` Stop a running planning task. @@ -172,7 +172,7 @@ Stop a running planning task. "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_stop", + "name": "plan_stop", "arguments": { "task_id": "task_abc123def456" } @@ -186,7 +186,7 @@ Stop a running planning task. ### Tool 5: `model_profiles` -Return available model profiles and their guidance before calling `task_create`. +Return available model profiles and their guidance before calling `plan_create`. **No required parameters:** @@ -205,7 +205,7 @@ Return available model profiles and their guidance before calling `task_create`. --- -### Tool 6: `task_file_info` +### Tool 6: `plan_file_info` Retrieve download information for completed plan artifacts. @@ -216,7 +216,7 @@ Retrieve download information for completed plan artifacts. "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_file_info", + "name": "plan_file_info", "arguments": { "task_id": "task_abc123def456", "artifact": "report" @@ -233,7 +233,7 @@ Retrieve download information for completed plan artifacts. --- -### Tool 7: `task_list` +### Tool 7: `plan_list` List recent tasks for an authenticated user. Useful for recovering a lost `task_id`. @@ -244,7 +244,7 @@ List recent tasks for an authenticated user. Useful for recovering a lost `task_ "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_list", + "name": "plan_list", "arguments": { "user_api_key": "pex_your_key_here", "limit": 10 @@ -257,7 +257,7 @@ List recent tasks for an authenticated user. Useful for recovering a lost `task_ --- -### Tool 8: `task_retry` +### Tool 8: `plan_retry` Retry a failed task with an optional upgraded model profile. @@ -268,7 +268,7 @@ Retry a failed task with an optional upgraded model profile. "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "task_retry", + "name": "plan_retry", "arguments": { "task_id": "task_abc123def456", "model_profile": "premium" @@ -287,12 +287,12 @@ Retry a failed task with an optional upgraded model profile. 2. Optionally call `model_profiles` to choose an appropriate `model_profile` 3. Formulate your planning prompt 4. Get user approval for the request -5. Call `task_create` with your prompt and parameters → receives `task_id` -6. Poll `task_status` every 5+ minutes until status is `completed` or `failed` -7. If `failed`, optionally call `task_retry` to requeue with a stronger model -8. Call `task_file_info` with completed `task_id` to get download link +5. Call `plan_create` with your prompt and parameters → receives `task_id` +6. Poll `plan_status` every 5+ minutes until status is `completed` or `failed` +7. If `failed`, optionally call `plan_retry` to requeue with a stronger model +8. Call `plan_file_info` with completed `task_id` to get download link 9. Download and use the generated plan -10. If you lose a `task_id`, call `task_list` with your `user_api_key` to recover it +10. If you lose a `task_id`, call `plan_list` with your `user_api_key` to recover it Refer to the [PlanExe API documentation](https://planexe.org/docs) for extended examples and advanced use cases.