Compare commits

..

54 Commits

Author SHA1 Message Date
openhands fc16da8fd2 Update microagent documentation to clarify that type field is optional 2025-06-08 19:39:17 +00:00
openhands bd3ff43c67 Remove name field from microagent files 2025-06-08 19:35:06 +00:00
openhands 0fe5b808af Update microagent code to use filename as name when not specified 2025-06-08 19:34:59 +00:00
openhands 6c49686ff0 Add MCP tools documentation and update microagent field requirements 2025-06-08 19:30:21 +00:00
openhands 17212bb2f2 Remove unused fields from microagent code and update all microagent files 2025-06-08 19:26:56 +00:00
openhands 9d9f931e95 Remove unused fields from microagent documentation and example 2025-06-08 19:23:47 +00:00
openhands 6fe9680474 Consolidate task microagent documentation into keyword-triggered microagents 2025-06-08 19:19:44 +00:00
Xingyao Wang 53c80d1c92 Merge branch 'main' into update-microagent-docs 2025-06-08 15:17:37 -04:00
openhands 401262f353 Update documentation for task microagents with user input support 2025-06-08 19:15:31 +00:00
Xingyao Wang 58845b01a3 rename more files 2025-06-08 14:30:37 -04:00
Xingyao Wang 469d184157 address engel comment 2025-06-08 14:28:22 -04:00
Xingyao Wang 4837c4dc74 Update microagents/get_test_to_pass.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-09 02:24:23 +08:00
Xingyao Wang 6763f21cc3 Merge branch 'main' into add-back-microagents 2025-06-07 16:47:00 -04:00
Xingyao Wang 32e610ac1d revert unnecessary change 2025-06-07 16:30:55 -04:00
Xingyao Wang 85c65391ca revert changes 2025-06-03 13:53:27 -04:00
Xingyao Wang c444dbfbbf remove fe changes 2025-06-03 12:04:37 -04:00
Xingyao Wang dd988d0f14 revert fe 2025-06-03 12:03:00 -04:00
Xingyao Wang 6f1a74e286 merge main 2025-06-03 11:37:51 -04:00
Xingyao Wang 7b956b6103 revert docs to look like main 2025-06-03 11:35:57 -04:00
openhands 34b097115d Fix linting issues in frontend and Python code 2025-05-19 01:39:48 +00:00
openhands 3e4ab4f379 Fix docstring formatting in KnowledgeMicroagent class 2025-05-19 01:29:23 +00:00
openhands 54cd9f7e44 Fix unlocalized strings in microagent-dropdown.tsx 2025-05-19 01:26:33 +00:00
openhands 802b765f98 Add microagent button and dropdown to trajectory actions 2025-05-17 12:05:13 +00:00
openhands 18c88f99ff Merge from main to resolve conflicts 2025-05-17 06:56:11 +00:00
openhands f3934be07b Fix microagent suggestions using tippy.js for better popup handling 2025-05-12 12:55:00 +00:00
openhands 6ce9f49d1e Fix linting issues in TipTap editor component 2025-05-12 11:06:15 +00:00
openhands fc07622b20 Implement microagent suggestions using TipTap 2025-05-12 11:00:08 +00:00
Xingyao Wang da935f9d8f Merge branch 'main' into add-back-microagents 2025-05-03 00:04:17 +08:00
openhands 642cc52a1a Fix linting issues in handlers.ts 2025-05-02 13:06:21 +00:00
openhands 4c361ab9e5 Add mock handler for microagents endpoint 2025-05-02 09:23:25 +00:00
openhands 5dfa1bb6eb Fix microagent suggestions UI and TypeScript errors 2025-05-02 09:21:15 +00:00
Xingyao Wang a07cf972a5 Merge commit '6032d2620d6ec252d3c80695a6de1fc88da9c87a' into add-back-microagents 2025-05-02 09:03:17 +00:00
openhands f2e3bc3254 Fix microagent suggestions feature 2025-05-02 08:52:19 +00:00
openhands 3790ec7d60 Add tests for microagent suggestions component 2025-05-02 03:31:41 +00:00
openhands 3c0719309e Add microagent suggestions feature to chat input 2025-05-02 02:57:57 +00:00
Xingyao Wang 0236e0943e fix test 2025-05-02 02:09:27 +00:00
Xingyao Wang cd464c0022 rename files 2025-05-01 10:38:04 +08:00
Xingyao Wang 4519a7f4f3 fix test 2025-05-01 02:29:52 +00:00
Xingyao Wang fdc591330b add remain 2025-05-01 02:25:38 +00:00
Xingyao Wang 98e454e82c fix lint and missing imports 2025-05-01 02:25:24 +00:00
Xingyao Wang e088d2d24a simplify microagent 2025-05-01 02:13:46 +00:00
Xingyao Wang 58c574af1e revert changes 2025-05-01 02:13:00 +00:00
Xingyao Wang 405f0069f8 revert some changes 2025-05-01 02:03:06 +00:00
Xingyao Wang f26d770d03 remove hardcoded last line 2025-05-01 02:01:51 +00:00
Xingyao Wang bf2c3de219 cleanup tests 2025-04-30 11:11:23 +08:00
Xingyao Wang 7c35ce16e5 Merge branch 'main' into add-back-microagents 2025-04-30 11:07:17 +08:00
Xingyao Wang f4024ccd94 Update microagents/update_pr_description.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:43:37 +08:00
Xingyao Wang b55bfed831 Update microagents/address_pr_comments.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:38:22 +08:00
OpenHands Bot cb0994027f 🤖 Auto-fix Python linting issues 2025-04-29 16:02:04 +00:00
openhands bcc9bd0b9a Move task microagent tests to test_microagent_task.py 2025-04-29 02:12:14 +00:00
openhands 6c144e6b5a Add back microagent files with special handling for user inputs 2025-04-29 02:06:42 +00:00
openhands e90b841b0d Update microagent files to match original ones with added triggers and variable prompts 2025-04-29 01:48:10 +00:00
openhands a1e6ed4dff Add special handling for microagents that require user input 2025-04-29 01:47:18 +00:00
openhands ad6311d3cd Add back microagent files and add special handling for user input variables 2025-04-29 01:33:23 +00:00
48 changed files with 573 additions and 638 deletions
-1
View File
@@ -33,7 +33,6 @@ body:
- Docker command in README
- GitHub resolver
- Development workflow
- CLI
- app.all-hands.dev
- Other
default: 0
+1
View File
@@ -16,6 +16,7 @@ updates:
mcp-packages:
patterns:
- "mcp"
- "mcpm"
security-all:
applies-to: "security-updates"
patterns:
+1 -1
View File
@@ -42,7 +42,7 @@
]
},
{
"group": "Running OpenHands on Your Own",
"group": "Running OpenHands Locally",
"pages": [
"usage/local-setup",
"usage/how-to/gui-mode",
+2 -15
View File
@@ -9,29 +9,16 @@ This mode is different from the [headless mode](./headless-mode), which is non-i
### Running with Python
1. Install OpenHands using pip:
```bash
pip install openhands-ai
```
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
2. Set your model, API key, and other preferences using environment variables or with the [`config.toml`](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) file.
3. Launch an interactive OpenHands conversation from the command line:
```bash
openhands
poetry run python -m openhands.cli.main
```
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
#### For Developers
If you have cloned the repository, you can run the CLI directly using Poetry:
```bash
poetry run python -m openhands.cli.main
```
### Running with Docker
1. Set the following environment variables in your terminal:
+1 -1
View File
@@ -46,7 +46,7 @@ This will produce a new image called `custom-image`, which will be available in
## Using the Docker Command
When running OpenHands using [the docker command](/usage/local-setup#start-the-app), replace
When running OpenHands using [the docker command](/usage/installation#start-the-app), replace
`-e SANDBOX_RUNTIME_CONTAINER_IMAGE=...` with `-e SANDBOX_BASE_CONTAINER_IMAGE=<custom image name>`:
```commandline
+1 -1
View File
@@ -48,6 +48,6 @@ The customization options you can set are:
| `LLM_MODEL` | Variable | Set the LLM to use with OpenHands | `LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"` |
| `OPENHANDS_MAX_ITER` | Variable | Set max limit for agent iterations | `OPENHANDS_MAX_ITER=10` |
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
| `TARGET_BRANCH` | Variable | Merge to branch other than `main` | `TARGET_BRANCH="dev"` |
| `TARGET_RUNNER` | Variable | Target runner to execute the agent workflow (default ubuntu-latest) | `TARGET_RUNNER="custom-runner"` |
+1 -1
View File
@@ -16,4 +16,4 @@ For more information see [getting started with OpenHands Cloud.](/usage/cloud/op
Run OpenHands on your local system and bring your own LLM and API key.
For more information see [running OpenHands on your own.](/usage/local-setup)
For more information see [running OpenHands locally.](/usage/local-setup)
+1 -1
View File
@@ -48,7 +48,7 @@ We recommend using [LMStudio](https://lmstudio.ai/) for serving these models loc
### Start OpenHands with locally served model
Check [the installation guide](/usage/local-setup) to make sure you have all the prerequisites for running OpenHands.
Check [the installation guide](https://docs.all-hands.dev/modules/usage/installation) to make sure you have all the prerequisites for running OpenHands.
```bash
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
+147 -8
View File
@@ -5,26 +5,111 @@ description: Keyword-triggered microagents provide OpenHands with specific instr
## Usage
These microagents are only loaded when a prompt includes one of the trigger words.
Keyword-triggered microagents are only loaded when a prompt includes one of the trigger words. There are two types of keyword-triggered microagents:
1. **Standard Keyword Microagents**: Triggered by keywords embedded in text
2. **Command-Style Microagents**: Triggered by command-style inputs (e.g., `/fix_test`) that can prompt for user input
Additionally, there's a special type of microagent that's always active:
3. **Repository Microagents**: Always active for a specific repository, providing repository-specific context and tools
## Frontmatter Syntax
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
above the guidelines.
above the guidelines. Enclose the frontmatter in triple dashes (---).
Enclose the frontmatter in triple dashes (---) and include the following fields:
### Standard Keyword Microagents
For standard keyword microagents, include the following fields:
| Field | Description | Required | Default |
|------------|--------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`knowledge`) | No | Inferred |
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
### Command-Style Microagents
## Example
For command-style microagents that require user input, include the following fields:
Keyword-triggered microagent file example located at `.openhands/microagents/yummy.md`:
```
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`task`) | No | Inferred |
| `triggers` | A list of command triggers (e.g., `/fix_test`) | No | `/[name]` |
| `inputs` | A list of input variables the microagent requires | Yes | None |
### Repository Microagents
Repository microagents are always active for a specific repository. They provide repository-specific context and tools.
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`repo`) | No | Inferred |
#### Repository Microagent Example
Here's an example of a repository microagent:
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
---
# Repository Guidelines
This repository follows these coding standards:
1. Use PEP 8 for Python code
2. Use ESLint for JavaScript code
3. Write unit tests for all new features
```
This microagent is always active when working with the repository and provides repository-specific guidelines.
### MCP Tools Support
Microagents can also provide additional MCP (Model-Code-Prompt) tools to the agent. This is useful for extending the agent's capabilities with custom tools.
| Field | Description | Required | Default |
|--------------|-----------------------------------------------------------|----------|------------------|
| `mcp_tools` | Configuration for additional MCP tools | No | None |
#### MCP Tools Example
Here's an example of a microagent that provides an additional MCP tool (the `fetch` tool for accessing web content):
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
mcp_tools:
stdio_servers:
- name: "fetch"
command: uvx
args:
- mcp-server-fetch
---
```
This microagent is a repository microagent (always active) that adds the `fetch` tool to the agent's capabilities.
Each input in the `inputs` list requires:
| Field | Description | Required |
|---------------|--------------------------------------------------|----------|
| `name` | The name of the input variable | Yes |
| `description` | A description of what the input should contain | Yes |
## Examples
### Standard Keyword Microagent Example
Standard keyword microagent file example located at `.openhands/microagents/yummy.md`:
```yaml
---
# The type field is optional and will be inferred as 'knowledge' when triggers are present
triggers:
- yummyhappy
- happyyummy
@@ -33,4 +118,58 @@ triggers:
The user has said the magic word. Respond with "That was delicious!"
```
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
### Command-Style Microagent Example
Command-style microagent file example located at `.openhands/microagents/fix_test.md`:
```yaml
---
# The type field is optional and will be inferred as 'task' when inputs are present
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
```
## Using Command-Style Microagents
Command-style microagents are designed to streamline common development tasks by providing structured templates for specific operations. They are triggered using a command-style format and will prompt the user for any required inputs.
### How to Use
1. Type `/` in the chat input to see available command-style microagents
2. Select a microagent from the dropdown or type its name (e.g., `/fix_test`)
3. The agent will prompt you for any required inputs
4. Provide the requested information
5. The agent will execute the task with your inputs
### Template Variables
In the body of a command-style microagent, you can reference input variables using the `{{ VARIABLE_NAME }}` syntax. These will be replaced with the user-provided values when the microagent is triggered.
### Available Command-Style Microagents
OpenHands includes several built-in command-style microagents:
| Command | Description |
|----------------------|-------------------------------------------------------|
| `/fix_test` | Fix failing tests by modifying a specific function |
| `/update_test` | Update tests for a new implementation |
| `/update_pr` | Update a pull request description |
| `/address_pr_comments` | Address comments on a pull request |
| `/add_repo_instruction` | Add instructions to the repository microagent |
[See examples of microagents in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
@@ -8,7 +8,7 @@ description: Microagents are specialized prompts that enhance OpenHands with dom
Currently OpenHands supports the following types of microagents:
- [General Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts, including command-style microagents that prompt for user inputs.
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
add `<microagent_name>.md` files inside. For repository-specific guidelines, you can ask OpenHands to analyze your repository and create a comprehensive `repo.md` file (see [General Microagents](./microagents-repo) for details).
@@ -34,7 +34,7 @@ some-repository/
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
is required:
| Microagent Type | Required |
|---------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |
| Microagent Type | Required |
|------------------------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents (all types)` | Yes |
@@ -96,7 +96,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
testId="llm-api-key-help-anchor"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
/>
</div>
+2 -2
View File
@@ -304,7 +304,7 @@ function LlmSettingsScreen() {
testId="llm-api-key-help-anchor"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
/>
<SettingsInput
@@ -379,7 +379,7 @@ function LlmSettingsScreen() {
testId="llm-api-key-help-anchor-advanced"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
/>
<SettingsInput
+2 -2
View File
@@ -12,7 +12,7 @@ export const TIPS: Tip[] = [
},
{
key: I18nKey.TIPS$SETUP_SCRIPT,
link: "https://docs.all-hands.dev/usage/prompting/repository#setup-script",
link: "https://docs.all-hands.dev/usage/customization/repository",
},
{ key: I18nKey.TIPS$VSCODE_INSTANCE },
{ key: I18nKey.TIPS$SAVE_WORK },
@@ -38,7 +38,7 @@ export const TIPS: Tip[] = [
},
{
key: I18nKey.TIPS$API_USAGE,
link: "https://docs.all-hands.dev/api-reference/health-check",
link: "https://docs.all-hands.dev/swagger-ui/",
},
];
+14 -16
View File
@@ -1,20 +1,17 @@
---
name: add_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
type: knowledge
---
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.
@@ -37,4 +34,5 @@ When creating a new microagent:
For detailed information, see:
- [Microagents Overview](https://docs.all-hands.dev/usage/prompting/microagents-overview)
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/github.md)
- [Microagents Syntax](https://docs.all-hands.dev/usage/prompting/microagents-syntax)
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/github.md)
+4 -8
View File
@@ -1,13 +1,9 @@
---
name: add_repo_inst
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: REPO_FOLDER_NAME
triggers:
- /add_repo_inst
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
@@ -62,4 +58,4 @@ Frontend:
```
Now, please write a similar markdown for the current repository.
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
+6 -10
View File
@@ -1,19 +1,15 @@
---
name: address_pr_comments
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: URL of the pull request
name: PR_URL
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
triggers:
- /address_pr_comments
inputs:
- name: PR_URL
description: "URL of the pull request"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
This branch corresponds to this PR {{ PR_URL }}
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.
+2 -5
View File
@@ -1,10 +1,7 @@
---
name: agent_memory
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- /remember
type: knowledge
---
* Repository memory: Use .openhands/microagents/repo.md under each repository root to store and access important information.
@@ -29,4 +26,4 @@ triggers:
- If you've only explored a portion of the codebase, clearly note this limitation in the repository structure documentation
- If you don't know the essential commands for working with the repository, such as lint or typecheck, ask the user and suggest adding them to repo.md for future reference (with permission)
When you receive this message, please review and summarize your recent actions and observations, then present a list of valuable information that should be saved in repo.md to the user.
When you receive this message, please review and summarize your recent actions and observations, then present a list of valuable information that should be saved in repo.md to the user.
+6 -12
View File
@@ -1,15 +1,9 @@
---
# This is a repo microagent that is always activated
# to include necessary default tools implemented with MCP
name: default-tools
type: repo
version: 1.0.0
agent: CodeActAgent
mcp_tools:
stdio_servers:
- name: "fetch"
command: "uvx"
args: ["mcp-server-fetch"]
# We leave the body empty because MCP tools will automatically add the
# tool description for LLMs in tool calls, so there's no need to add extra descriptions.
---
- args:
- mcp-server-fetch
command: uvx
name: fetch
type: repo
---
+2 -5
View File
@@ -1,11 +1,8 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container
type: knowledge
---
# Docker Installation and Usage Guide
@@ -52,4 +49,4 @@ To verify Docker is working correctly, run the hello-world container:
```bash
sudo docker run hello-world
```
```
+11 -14
View File
@@ -1,23 +1,20 @@
---
name: fix_test
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
- description: The name of function to fix
name: FUNCTION_TO_FIX
- description: The path of the file that contains the function
name: FILE_FOR_FUNCTION
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
+2 -5
View File
@@ -1,11 +1,8 @@
---
name: flarglebargle
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- flarglebargle
type: knowledge
---
IMPORTANT! The user has said the magic word "flarglebargle". You must
only respond with a message telling them how smart they are
only respond with a message telling them how smart they are
+2 -5
View File
@@ -1,11 +1,8 @@
---
name: github
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- github
- git
type: knowledge
---
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
@@ -32,4 +29,4 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
```bash
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
```
```
+2 -5
View File
@@ -1,11 +1,8 @@
---
name: gitlab
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- gitlab
- git
type: knowledge
---
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
@@ -31,4 +28,4 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
```bash
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
```
```
+2 -5
View File
@@ -1,12 +1,9 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s
- kube
type: knowledge
---
# Kubernetes Local Development with KIND
@@ -47,4 +44,4 @@ Create a basic KIND cluster:
```bash
kind create cluster
```
```
+2 -5
View File
@@ -1,11 +1,8 @@
---
name: npm
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- npm
type: knowledge
---
When using npm to install packages, you will not be able to use an interactive shell, and it may be hard to confirm your actions.
As an alternative, you can pipe in the output of the unix "yes" command to confirm your actions.
As an alternative, you can pipe in the output of the unix "yes" command to confirm your actions.
+2 -5
View File
@@ -1,10 +1,7 @@
---
name: pdflatex
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- pdflatex
type: knowledge
---
PdfLatex is a tool that converts Latex sources into PDF. This is specifically very important for researchers, as they use it to publish their findings. It could be installed very easily using Linux terminal, though this seems an annoying task on Windows. Installation commands are given below.
@@ -33,4 +30,4 @@ Once installed as above, you may be able to create PDF files from latex sources
pdflatex latex_source_name.tex
```
Ref: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html
Ref: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html
+8 -10
View File
@@ -1,15 +1,13 @@
---
name: security
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- security
- vulnerability
- authentication
- authorization
- permissions
- security
- vulnerability
- authentication
- authorization
- permissions
type: knowledge
---
This document provides guidance on security best practices
You should always be considering security implications when developing.
@@ -31,4 +29,4 @@ You should always complete the task requested. If there are security concerns pl
- Never expose sensitive information in error messages
- Log security events appropriately
- Implement proper exception handling
- Use secure error reporting mechanisms
- Use secure error reporting mechanisms
+9 -12
View File
@@ -1,16 +1,13 @@
---
name: SSH Microagent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
type: knowledge
---
# SSH Microagent
@@ -134,4 +131,4 @@ chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
# Set correct permissions for SSH directory
chmod 700 ~/.ssh
```
```
+6 -9
View File
@@ -1,12 +1,9 @@
---
name: swift-linux
type: knowledge
agent: CodeActAgent
version: 1.0.0
triggers:
- swift-linux
- swift-debian
- swift-installation
triggers:
- swift-linux
- swift-debian
- swift-installation
type: knowledge
---
# Swift Installation Guide for Debian Linux
@@ -80,4 +77,4 @@ Verify that Swift is correctly installed by running:
```bash
swift --version
```
```
+10 -14
View File
@@ -1,21 +1,17 @@
---
name: update_pr_description
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: URL of the pull request
name: PR_URL
type: string
validation:
pattern: ^https://github.com/.+/.+/pull/[0-9]+$
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
type: string
triggers:
- /update_pr_description
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.
+7 -10
View File
@@ -1,19 +1,16 @@
---
name: update_test
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
triggers:
- /update_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
The current implementation of the code is correct BUT the test functions {{ FUNCTION_TO_FIX }} in file {{ FILE_FOR_FUNCTION }} are failing.
Please update the test file so that they pass with the current version of the implementation.
Please update the test file so that they pass with the current version of the implementation.
+26 -95
View File
@@ -12,7 +12,7 @@ from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.application import Application
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import HTML, StyleAndTextTuples
from prompt_toolkit.formatted_text import HTML, FormattedText, StyleAndTextTuples
from prompt_toolkit.input import create_input
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
@@ -132,113 +132,51 @@ def display_initialization_animation(text: str, is_loaded: asyncio.Event) -> Non
def display_banner(session_id: str) -> None:
banner_text = r"""<gold>
print_formatted_text(
HTML(r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""
# Use TextArea with focusable=True to allow text selection
banner_container = Frame(
TextArea(
text=banner_text.replace('<gold>', '').replace('</gold>', ''),
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
</gold>"""),
style=DEFAULT_STYLE,
)
print_container(banner_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
print_formatted_text('')
version_container = Frame(
TextArea(
text=f'OpenHands CLI v{__version__}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_container(version_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
session_container = Frame(
TextArea(
text=f'Initialized conversation {session_id}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_formatted_text('')
print_container(session_container)
print_formatted_text(HTML(f'<grey>Initialized conversation {session_id}</grey>'))
print_formatted_text('')
def display_welcome_message(message: str = '') -> None:
# Use TextArea with focusable=True to allow text selection
welcome_container = Frame(
TextArea(
text="Let's start building!",
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
)
print_container(welcome_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
if message:
message_text = f'{message} Type /help for help'
print_formatted_text(
HTML(f'{message} <grey>Type /help for help</grey>'),
style=DEFAULT_STYLE,
)
else:
message_text = 'What do you want to build? Type /help for help'
message_container = Frame(
TextArea(
text=message_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GREY}',
)
print_container(message_container)
# Call print_formatted_text to maintain compatibility with tests
print_formatted_text('')
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>'),
style=DEFAULT_STYLE,
)
def display_initial_user_prompt(prompt: str) -> None:
# Use TextArea with focusable=True to allow text selection
prompt_container = Frame(
TextArea(
text=f'> {prompt}',
read_only=True,
style=COLOR_GOLD,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
style=f'fg:{COLOR_GOLD}',
print_formatted_text(
FormattedText(
[
('', '\n'),
(COLOR_GOLD, '> '),
('', prompt),
]
)
)
print_formatted_text('')
print_container(prompt_container)
# Prompt output display functions
@@ -286,7 +224,6 @@ def display_error(error: str) -> None:
read_only=True,
style='ansired',
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Error',
style='ansired',
@@ -302,7 +239,6 @@ def display_command(event: CmdRunAction) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Command',
style='ansiblue',
@@ -331,7 +267,6 @@ def display_command_output(output: str) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Command Output',
style=f'fg:{COLOR_GREY}',
@@ -347,7 +282,6 @@ def display_file_edit(event: FileEditObservation) -> None:
read_only=True,
wrap_lines=True,
lexer=CustomDiffLexer(),
focusable=True, # Allow focusing to enable text selection
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
@@ -364,7 +298,6 @@ def display_file_read(event: FileReadObservation) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='File Read',
style=f'fg:{COLOR_GREY}',
@@ -383,7 +316,6 @@ def initialize_streaming_output():
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
)
container = Frame(
streaming_output_text_area,
@@ -493,7 +425,6 @@ def display_usage_metrics(usage_metrics: UsageMetrics) -> None:
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
focusable=True, # Allow focusing to enable text selection
),
title='Usage Metrics',
style=f'fg:{COLOR_GREY}',
-4
View File
@@ -3,7 +3,6 @@ from typing import Iterable
from urllib.parse import urlencode
import httpx # type: ignore
from fastapi import status
from openhands.events.event import Event
from openhands.events.event_filter import EventFilter
@@ -43,9 +42,6 @@ class NestedEventStore(EventStoreABC):
if self.session_api_key:
headers['X-Session-API-Key'] = self.session_api_key
response = httpx.get(url, headers=headers)
if response.status_code == status.HTTP_404_NOT_FOUND:
# Follow pattern of event store not throwing errors on not found
return
result_set = response.json()
for result in result_set['events']:
event = event_from_dict(result)
+8 -2
View File
@@ -103,7 +103,9 @@ class BaseMicroagent(BaseModel):
if metadata.inputs:
inferred_type = MicroagentType.TASK
# Add a trigger for the agent name if not already present
trigger = f'/{metadata.name}'
# Use derived_name if available, otherwise use metadata.name
agent_name = derived_name if derived_name is not None and (metadata.name == 'default' or not metadata.name) else metadata.name
trigger = f'/{agent_name}'
if not metadata.triggers or trigger not in metadata.triggers:
if not metadata.triggers:
metadata.triggers = [trigger]
@@ -121,7 +123,11 @@ class BaseMicroagent(BaseModel):
raise ValueError(f'Could not determine microagent type for: {path}')
# Use derived_name if available (from relative path), otherwise fallback to metadata.name
agent_name = derived_name if derived_name is not None else metadata.name
# If metadata.name is still the default 'default', use the derived_name
if derived_name is not None and (metadata.name == 'default' or not metadata.name):
agent_name = derived_name
else:
agent_name = metadata.name
agent_class = subclass_map[inferred_type]
return agent_class(
+4 -2
View File
@@ -27,8 +27,10 @@ class MicroagentMetadata(BaseModel):
name: str = 'default'
type: MicroagentType = Field(default=MicroagentType.REPO_KNOWLEDGE)
version: str = Field(default='1.0.0')
agent: str = Field(default='CodeActAgent')
# Keep these fields for backward compatibility but they're not used
version: str = Field(default='1.0.0', exclude=True)
agent: str = Field(default='CodeActAgent', exclude=True)
author: str = Field(default='', exclude=True)
triggers: list[str] = [] # optional, only exists for knowledge microagents
inputs: list[InputMetadata] = [] # optional, only exists for task microagents
mcp_tools: MCPConfig | None = (
+102 -50
View File
@@ -9,6 +9,7 @@ import argparse
import asyncio
import base64
import json
import logging
import mimetypes
import os
import shutil
@@ -25,6 +26,8 @@ from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import APIKeyHeader
from mcpm import MCPRouter, RouterConfig
from mcpm.router.router import logger as mcp_router_logger
from openhands_aci.editor.editor import OHEditor
from openhands_aci.editor.exceptions import ToolError
from openhands_aci.editor.results import ToolResult
@@ -34,7 +37,6 @@ from starlette.background import BackgroundTask
from starlette.exceptions import HTTPException as StarletteHTTPException
from uvicorn import run
from openhands.core.config.mcp_config import MCPStdioServerConfig
from openhands.core.exceptions import BrowserUnavailableException
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
@@ -61,18 +63,20 @@ from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.runtime.browser import browse
from openhands.runtime.browser.browser_env import BrowserEnv
from openhands.runtime.file_viewer_server import start_file_viewer_server
# Import our custom MCP Proxy Manager
from openhands.runtime.mcp.proxy import MCPProxyManager
from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin
from openhands.runtime.utils import find_available_tcp_port
from openhands.runtime.utils.bash import BashSession
from openhands.runtime.utils.files import insert_lines, read_lines
from openhands.runtime.utils.log_capture import capture_logs
from openhands.runtime.utils.memory_monitor import MemoryMonitor
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
from openhands.runtime.utils.system_stats import get_system_stats
from openhands.utils.async_utils import call_sync_from_async, wait_all
# Set MCP router logger to the same level as the main logger
mcp_router_logger.setLevel(logger.getEffectiveLevel())
if sys.platform == 'win32':
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
@@ -467,7 +471,7 @@ class ActionExecutor:
filepath = self._resolve_path(action.path, working_dir)
try:
if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
with open(filepath, 'rb') as file:
with open(filepath, 'rb') as file: # noqa: ASYNC101
image_data = file.read()
encoded_image = base64.b64encode(image_data).decode('utf-8')
mime_type, _ = mimetypes.guess_type(filepath)
@@ -477,13 +481,13 @@ class ActionExecutor:
return FileReadObservation(path=filepath, content=encoded_image)
elif filepath.lower().endswith('.pdf'):
with open(filepath, 'rb') as file:
with open(filepath, 'rb') as file: # noqa: ASYNC101
pdf_data = file.read()
encoded_pdf = base64.b64encode(pdf_data).decode('utf-8')
encoded_pdf = f'data:application/pdf;base64,{encoded_pdf}'
return FileReadObservation(path=filepath, content=encoded_pdf)
elif filepath.lower().endswith(('.mp4', '.webm', '.ogg')):
with open(filepath, 'rb') as file:
with open(filepath, 'rb') as file: # noqa: ASYNC101
video_data = file.read()
encoded_video = base64.b64encode(video_data).decode('utf-8')
mime_type, _ = mimetypes.guess_type(filepath)
@@ -493,7 +497,7 @@ class ActionExecutor:
return FileReadObservation(path=filepath, content=encoded_video)
with open(filepath, 'r', encoding='utf-8') as file:
with open(filepath, 'r', encoding='utf-8') as file: # noqa: ASYNC101
lines = read_lines(file.readlines(), action.start, action.end)
except FileNotFoundError:
return ErrorObservation(
@@ -526,7 +530,7 @@ class ActionExecutor:
mode = 'w' if not file_exists else 'r+'
try:
with open(filepath, mode, encoding='utf-8') as file:
with open(filepath, mode, encoding='utf-8') as file: # noqa: ASYNC101
if mode != 'w':
all_lines = file.readlines()
new_file = insert_lines(insert, all_lines, action.start, action.end)
@@ -650,11 +654,14 @@ if __name__ == '__main__':
plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore
client: ActionExecutor | None = None
mcp_proxy_manager: MCPProxyManager | None = None
mcp_router: MCPRouter | None = None
MCP_ROUTER_PROFILE_PATH = os.path.join(
os.path.dirname(__file__), 'mcp', 'config.json'
)
@asynccontextmanager
async def lifespan(app: FastAPI):
global client, mcp_proxy_manager
global client, mcp_router
logger.info('Initializing ActionExecutor...')
client = ActionExecutor(
plugins_to_load,
@@ -669,36 +676,63 @@ if __name__ == '__main__':
# Check if we're on Windows
is_windows = sys.platform == 'win32'
# Initialize and mount MCP Proxy Manager (skip on Windows)
# Initialize and mount MCP Router (skip on Windows)
if is_windows:
logger.info('Skipping MCP Proxy initialization on Windows')
mcp_proxy_manager = None
logger.info('Skipping MCP Router initialization on Windows')
mcp_router = None
else:
logger.info('Initializing MCP Proxy Manager...')
# Create a MCP Proxy Manager
mcp_proxy_manager = MCPProxyManager(
auth_enabled=bool(SESSION_API_KEY),
api_key=SESSION_API_KEY,
logger_level=logger.getEffectiveLevel(),
logger.info('Initializing MCP Router...')
mcp_router = MCPRouter(
profile_path=MCP_ROUTER_PROFILE_PATH,
router_config=RouterConfig(
api_key=SESSION_API_KEY,
auth_enabled=bool(SESSION_API_KEY),
),
)
mcp_proxy_manager.initialize()
# Mount the proxy to the app
allowed_origins = ['*']
try:
await mcp_proxy_manager.mount_to_app(app, allowed_origins)
except Exception as e:
logger.error(f'Error mounting MCP Proxy: {e}', exc_info=True)
raise RuntimeError(f'Cannot mount MCP Proxy: {e}')
sse_app = await mcp_router.get_sse_server_app(
allow_origins=allowed_origins, include_lifespan=False
)
# Only mount SSE app if MCP Router is initialized (not on Windows)
if mcp_router is not None:
# Check for route conflicts before mounting
main_app_routes = {route.path for route in app.routes}
sse_app_routes = {route.path for route in sse_app.routes}
conflicting_routes = main_app_routes.intersection(sse_app_routes)
if conflicting_routes:
logger.error(f'Route conflicts detected: {conflicting_routes}')
raise RuntimeError(
f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}'
)
app.mount('/', sse_app)
logger.info(
f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}'
)
# Additional debug logging
if logger.isEnabledFor(logging.DEBUG):
logger.debug('Main app routes:')
for route in main_app_routes:
logger.debug(f' {route}')
logger.debug('MCP SSE server app routes:')
for route in sse_app_routes:
logger.debug(f' {route}')
yield
# Clean up & release the resources
logger.info('Shutting down MCP Proxy Manager...')
if mcp_proxy_manager:
del mcp_proxy_manager
mcp_proxy_manager = None
logger.info('Shutting down MCP Router...')
if mcp_router:
try:
await mcp_router.shutdown()
logger.info('MCP Router shutdown successfully.')
except Exception as e:
logger.error(f'Error shutting down MCP Router: {e}', exc_info=True)
else:
logger.info('MCP Proxy Manager instance not found for shutdown.')
logger.info('MCP Router instance not found for shutdown.')
logger.info('Closing ActionExecutor...')
if client:
@@ -790,9 +824,6 @@ if __name__ == '__main__':
# Check if we're on Windows
is_windows = sys.platform == 'win32'
# Access the global mcp_proxy_manager variable
global mcp_proxy_manager
if is_windows:
# On Windows, just return a success response without doing anything
logger.info(
@@ -807,10 +838,17 @@ if __name__ == '__main__':
)
# Non-Windows implementation
if mcp_proxy_manager is None:
raise HTTPException(
status_code=500, detail='MCP Proxy Manager is not initialized'
)
assert mcp_router is not None
assert os.path.exists(MCP_ROUTER_PROFILE_PATH)
# Use synchronous file operations outside of async function
def read_profile():
with open(MCP_ROUTER_PROFILE_PATH, 'r') as f:
return json.load(f)
current_profile = read_profile()
assert 'default' in current_profile
assert isinstance(current_profile['default'], list)
# Get the request body
mcp_tools_to_sync = await request.json()
@@ -818,17 +856,31 @@ if __name__ == '__main__':
raise HTTPException(
status_code=400, detail='Request must be a list of MCP tools to sync'
)
logger.info(
f'Updating MCP server with tools: {json.dumps(mcp_tools_to_sync, indent=2)}'
f'Updating MCP server to: {json.dumps(mcp_tools_to_sync, indent=2)}.\nPrevious profile: {json.dumps(current_profile, indent=2)}'
)
mcp_tools_to_sync = [MCPStdioServerConfig(**tool) for tool in mcp_tools_to_sync]
try:
await mcp_proxy_manager.update_and_remount(app, mcp_tools_to_sync, ['*'])
logger.info('MCP Proxy Manager updated and remounted successfully')
router_error_log = ''
except Exception as e:
logger.error(f'Error updating MCP Proxy Manager: {e}', exc_info=True)
router_error_log = str(e)
current_profile['default'] = mcp_tools_to_sync
# Use synchronous file operations outside of async function
def write_profile(profile):
with open(MCP_ROUTER_PROFILE_PATH, 'w') as f:
json.dump(profile, f)
write_profile(current_profile)
# Manually reload the profile and update the servers
mcp_router.profile_manager.reload()
servers_wait_for_update = mcp_router.get_unique_servers()
async with capture_logs('mcpm.router.router') as log_capture:
await mcp_router.update_servers(servers_wait_for_update)
router_error_log = log_capture.getvalue()
logger.info(
f'MCP router updated successfully with unique servers: {servers_wait_for_update}'
)
if router_error_log:
logger.warning(f'Some MCP servers failed to be added: {router_error_log}')
return JSONResponse(
status_code=200,
@@ -863,7 +915,7 @@ if __name__ == '__main__':
)
zip_path = os.path.join(full_dest_path, file.filename)
with open(zip_path, 'wb') as buffer:
with open(zip_path, 'wb') as buffer: # noqa: ASYNC101
shutil.copyfileobj(file.file, buffer)
# Extract the zip file
@@ -876,7 +928,7 @@ if __name__ == '__main__':
else:
# For single file uploads
file_path = os.path.join(full_dest_path, file.filename)
with open(file_path, 'wb') as buffer:
with open(file_path, 'wb') as buffer: # noqa: ASYNC101
shutil.copyfileobj(file.file, buffer)
logger.debug(f'Uploaded file {file.filename} to {destination}')
@@ -435,7 +435,7 @@ class ActionExecutionClient(Runtime):
# We should always include the runtime as an MCP server whenever there's > 0 stdio servers
updated_mcp_config.sse_servers.append(
MCPSSEServerConfig(
url=self.action_execution_server_url.rstrip('/') + '/mcp/sse',
url=self.action_execution_server_url.rstrip('/') + '/sse',
api_key=self.session_api_key,
)
)
+1 -4
View File
@@ -1,6 +1,3 @@
{
"mcpServers": {
"default": {}
},
"tools": []
"default": []
}
-71
View File
@@ -1,71 +0,0 @@
# MCP Proxy Manager
This module provides a manager class for handling FastMCP proxy instances in OpenHands, including initialization, configuration, and mounting to FastAPI applications.
## Overview
The `MCPProxyManager` class encapsulates all the functionality related to creating, configuring, and managing FastMCP proxy instances. It simplifies the process of:
1. Initializing a FastMCP proxy
2. Configuring the proxy with tools
3. Mounting the proxy to a FastAPI application
4. Updating the proxy configuration
5. Shutting down the proxy
## Usage
### Basic Usage
```python
from openhands.runtime.mcp.proxy import MCPProxyManager
from fastapi import FastAPI
# Create a FastAPI app
app = FastAPI()
# Create a proxy manager
proxy_manager = MCPProxyManager(
name="MyProxyServer",
auth_enabled=True,
api_key="my-api-key"
)
# Initialize the proxy
proxy_manager.initialize()
# Mount the proxy to the app
await proxy_manager.mount_to_app(app, allow_origins=["*"])
# Update the tools configuration
tools = [
{
"name": "my_tool",
"description": "My tool description",
"parameters": {...}
}
]
proxy_manager.update_tools(tools)
# Update and remount the proxy
await proxy_manager.update_and_remount(app, tools, allow_origins=["*"])
# Shutdown the proxy
await proxy_manager.shutdown()
```
### In-Memory Configuration
The `MCPProxyManager` maintains the configuration in-memory, eliminating the need for file-based configuration. This makes it easier to update the configuration and reduces the complexity of the code.
## Benefits
1. **Simplified API**: The `MCPProxyManager` provides a simple and intuitive API for managing FastMCP proxies.
2. **In-Memory Configuration**: Configuration is maintained in-memory, eliminating the need for file I/O operations.
3. **Improved Error Handling**: The manager provides better error handling and logging for proxy operations.
4. **Cleaner Code**: By encapsulating proxy-related functionality in a dedicated class, the code is more maintainable and easier to understand.
## Implementation Details
The `MCPProxyManager` uses the `FastMCP.as_proxy()` method to create a proxy server. It manages the lifecycle of the proxy, including initialization, configuration updates, and shutdown.
When updating the tools configuration, the manager creates a new proxy with the updated configuration and remounts it to the FastAPI application, ensuring that the proxy is always up-to-date with the latest configuration.
-7
View File
@@ -1,7 +0,0 @@
"""
MCP Proxy module for OpenHands.
"""
from openhands.runtime.mcp.proxy.manager import MCPProxyManager
__all__ = ['MCPProxyManager']
-138
View File
@@ -1,138 +0,0 @@
"""
MCP Proxy Manager for OpenHands.
This module provides a manager class for handling FastMCP proxy instances,
including initialization, configuration, and mounting to FastAPI applications.
"""
import logging
from typing import Any, Optional
from fastapi import FastAPI
from fastmcp import FastMCP
from fastmcp.utilities.logging import get_logger as fastmcp_get_logger
from openhands.core.config.mcp_config import MCPStdioServerConfig
logger = logging.getLogger(__name__)
fastmcp_logger = fastmcp_get_logger('fastmcp')
class MCPProxyManager:
"""
Manager for FastMCP proxy instances.
This class encapsulates all the functionality related to creating, configuring,
and managing FastMCP proxy instances, including mounting them to FastAPI applications.
"""
def __init__(
self,
auth_enabled: bool = False,
api_key: Optional[str] = None,
logger_level: Optional[int] = None,
):
"""
Initialize the MCP Proxy Manager.
Args:
name: Name of the proxy server
auth_enabled: Whether authentication is enabled
api_key: API key for authentication (required if auth_enabled is True)
logger_level: Logging level for the FastMCP logger
"""
self.auth_enabled = auth_enabled
self.api_key = api_key
self.proxy: Optional[FastMCP] = None
# Initialize with a valid configuration format for FastMCP
self.config: dict[str, Any] = {
'mcpServers': {},
}
# Configure FastMCP logger
if logger_level is not None:
fastmcp_logger.setLevel(logger_level)
def initialize(self) -> None:
"""
Initialize the FastMCP proxy with the current configuration.
"""
if len(self.config['mcpServers']) == 0:
logger.info(
f"No MCP servers configured for FastMCP Proxy, skipping initialization."
)
return None
# Create a new proxy with the current configuration
self.proxy = FastMCP.as_proxy(
self.config,
auth_enabled=self.auth_enabled,
api_key=self.api_key,
)
logger.info(f"FastMCP Proxy initialized successfully")
async def mount_to_app(
self, app: FastAPI, allow_origins: Optional[list[str]] = None
) -> None:
"""
Mount the SSE server app to a FastAPI application.
Args:
app: FastAPI application to mount to
allow_origins: List of allowed origins for CORS
"""
if len(self.config['mcpServers']) == 0:
logger.info(
f"No MCP servers configured for FastMCP Proxy, skipping mount."
)
return
if not self.proxy:
raise ValueError('FastMCP Proxy is not initialized')
# Get the SSE app
# mcp_app = self.proxy.http_app(path='/shttp')
mcp_app = self.proxy.http_app(path='/sse', transport='sse')
app.mount('/mcp', mcp_app)
# Remove any existing mounts at root path
if '/mcp' in app.routes:
app.routes.remove('/mcp')
app.mount('/', mcp_app)
logger.info(f"Mounted FastMCP Proxy app at /mcp")
async def update_and_remount(
self,
app: FastAPI,
stdio_servers: list[MCPStdioServerConfig],
allow_origins: Optional[list[str]] = None,
) -> None:
"""
Update the tools configuration and remount the proxy to the app.
This is a convenience method that combines updating the tools,
shutting down the existing proxy, initializing a new one, and
mounting it to the app.
Args:
app: FastAPI application to mount to
tools: List of tool configurations
allow_origins: List of allowed origins for CORS
"""
tools = {
t.name: t.model_dump()
for t in stdio_servers
}
self.config['mcpServers'] = tools
del self.proxy
self.proxy = None
# Initialize a new proxy
self.initialize()
# Mount the new proxy to the app
await self.mount_to_app(app, allow_origins)
@@ -295,14 +295,6 @@ class DockerNestedConversationManager(ConversationManager):
) as client:
response = await client.post(f'{nested_url}/api/conversations/{sid}/stop')
response.raise_for_status()
# Check up to 3 times that client has closed
for _ in range(3):
response = await client.get(f'{nested_url}/api/conversations/{sid}')
if response.status_code == status.HTTP_200_OK and response.json().get('status') == "STOPPED":
break
await asyncio.sleep(1)
except Exception:
logger.exception("error_stopping_container")
container.stop()
Generated
+151 -5
View File
@@ -2116,6 +2116,52 @@ files = [
{file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"},
]
[[package]]
name = "duckdb"
version = "1.3.0"
description = "DuckDB in-process database"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc65c1e97aa010359c43c0342ea423e6efa3cb8c8e3f133b0765451ce674e3db"},
{file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8fc91b629646679e33806342510335ccbbeaf2b823186f0ae829fd48e7a63c66"},
{file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:1a69b970553fd015c557238d427ef00be3c8ed58c3bc3641aef987e33f8bf614"},
{file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1003e84c07b84680cee6d06e4795b6e861892474704f7972058594a52c7473cf"},
{file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:992239b54ca6f015ad0ed0d80f3492c065313c4641df0a226183b8860cb7f5b0"},
{file = "duckdb-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ba1c5af59e8147216149b814b1970b8f7e3c240494a9688171390db3c504b29"},
{file = "duckdb-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b794ca28e22b23bd170506cb1d4704a3608e67f0fe33273db9777b69bdf26a"},
{file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:60a58b85929754abb21db1e739d2f53eaef63e6015e62ba58eae3425030e7935"},
{file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:1d46b5a20f078b1b2284243e02a1fde7e12cbb8d205fce62e4700bcfe6a09881"},
{file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0044e5ffb2d46308099640a92f99980a44e12bb68642aa9e6b08acbf300d64a1"},
{file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cb813de2ca2f5e7c77392a67bdcaa174bfd69ebbfdfc983024af270c77a0447"},
{file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a0c993eb6df2b30b189ad747f3aea1b0b87b78ab7f80c6e7c57117b6e8dbfb0"},
{file = "duckdb-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6728e209570d36ece66dd7249e5d6055326321137cd807f26300733283930cd4"},
{file = "duckdb-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e652b7c8dbdb91a94fd7d543d3e115d24a25aa0791a373a852e20cb7bb21154"},
{file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f24038fe9b83dcbaeafb1ed76ec3b3f38943c1c8d27ab464ad384db8a6658b61"},
{file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:956c85842841bef68f4a5388c6b225b933151a7c06d568390fc895fc44607913"},
{file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:efe883d822ed56fcfbb6a7b397c13f6a0d2eaeb3bc4ef4510f84fadb3dfe416d"},
{file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3872a3a1b80ffba5264ea236a3754d0c41d3c7b01bdf8cdcb1c180fc1b8dc8e2"},
{file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30bf45ad78a5a997f378863e036e917b481d18d685e5c977cd0a3faf2e31fbaf"},
{file = "duckdb-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85cbd8e1d65df8a0780023baf5045d3033fabd154799bc9ea6d9ab5728f41eb3"},
{file = "duckdb-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8754c40dac0f26d9fb0363bbb5df02f7a61ce6a6728d5efc02c3bc925d7c89c3"},
{file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:176b9818d940c52ac7f31c64a98cf172d7c19d2a006017c9c4e9c06c246e36bf"},
{file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:03981f7e8793f07a4a9a2ba387640e71d0a99ebcaf8693ab09f96d59e628b713"},
{file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:a177d55a38a62fdf79b59a0eaa32531a1dbb443265f6d67f64992cc1e82b755c"},
{file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1c30e3749823147d5578bc3f01f35d1a0433a1c768908d946056ec8d6e1757e"},
{file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5855f3a564baf22eeeab70c120b51f5a11914f1f1634f03382daeb6b1dea4c62"},
{file = "duckdb-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1fac15a48056f7c2739cf8800873063ba2f691e91a9b2fc167658a401ca76a"},
{file = "duckdb-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:fbdfc1c0b83b90f780ae74038187ee696bb56ab727a289752372d7ec42dda65b"},
{file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5f6b5d725546ad30abc125a6813734b493fea694bc3123e991c480744573c2f1"},
{file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:fcbcc9b956b06cf5ee94629438ecab88de89b08b5620fcda93665c222ab18cd4"},
{file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2d32f2d44105e1705d8a0fb6d6d246fd69aff82c80ad23293266244b66b69012"},
{file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aa7a5c0dcb780850e6da1227fb1d552af8e1a5091e02667ab6ace61ab49ce6c"},
{file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cb254fd5405f3edbd7d962ba39c72e4ab90b37cb4d0e34846089796c8078419"},
{file = "duckdb-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7d337b58c59fd2cd9faae531b05d940f8d92bdc2e14cb6e9a5a37675ad2742d"},
{file = "duckdb-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3cea3a345755c7dbcb58403dbab8befd499c82f0d27f893a4c1d4b8cf56ec54"},
{file = "duckdb-1.3.0.tar.gz", hash = "sha256:09aaa4b1dca24f4d1f231e7ae66b6413e317b7e04e2753541d42df6c8113fac7"},
]
[[package]]
name = "dulwich"
version = "0.22.8"
@@ -2923,8 +2969,8 @@ files = [
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@@ -2946,8 +2992,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0"
grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
requests = ">=2.18.0,<3.0.0"
@@ -3165,8 +3211,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
proto-plus = [
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
@@ -5352,6 +5398,30 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"]
rich = ["rich (>=13.9.4)"]
ws = ["websockets (>=15.0.1)"]
[[package]]
name = "mcpm"
version = "1.12.0"
description = "MCPM - Model Context Protocol Manager"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "mcpm-1.12.0-py3-none-any.whl", hash = "sha256:ed3a87300420bcdb9cd12ef290179fda5bd51eb2f4cd3e793084d83eed91b249"},
{file = "mcpm-1.12.0.tar.gz", hash = "sha256:e9d2b852b90d7fd62dede584f035dd6b2b3d068d233e96b82aead835f81a911a"},
]
[package.dependencies]
click = ">=8.1.3"
duckdb = ">=1.2.2"
mcp = ">=1.8.0"
prompt-toolkit = ">=3.0.0"
psutil = ">=7.0.0"
pydantic = ">=2.5.1"
requests = ">=2.28.0"
rich = ">=12.0.0"
ruamel-yaml = ">=0.18.10"
watchfiles = ">=1.0.4"
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -6430,8 +6500,8 @@ files = [
[package.dependencies]
googleapis-common-protos = ">=1.52,<2.0"
grpcio = [
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
]
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.34.0"
@@ -8867,6 +8937,82 @@ files = [
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "ruamel-yaml"
version = "0.18.12"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "ruamel.yaml-0.18.12-py3-none-any.whl", hash = "sha256:790ba4c48b6a6e6b12b532a7308779eb12d2aaab3a80fdb8389216f28ea2b287"},
{file = "ruamel.yaml-0.18.12.tar.gz", hash = "sha256:5a38fd5ce39d223bebb9e3a6779e86b9427a03fb0bf9f270060f8b149cffe5e2"},
]
[package.dependencies]
"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""}
[package.extras]
docs = ["mercurial (>5.7)", "ryd"]
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
[[package]]
name = "ruamel-yaml-clib"
version = "0.2.12"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "platform_python_implementation == \"CPython\""
files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
]
[[package]]
name = "ruff"
version = "0.11.11"
@@ -11614,4 +11760,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "8c960ca43a540bfd96dc029d45fa4e0a4a3f75c2996feecaa8b989c348655f70"
content-hash = "d9f6c24fa80dd191f180af0c802ea11ecf514d86aaa421cb19a9bb497362c101"
+1
View File
@@ -67,6 +67,7 @@ poetry = "^2.1.2"
anyio = "4.9.0"
pythonnet = "*"
fastmcp = "^2.5.2"
mcpm = "1.12.0"
python-frontmatter = "^1.1.0"
# TODO: Should these go into the runtime group?
ipywidgets = "^8.1.5"
+5 -13
View File
@@ -114,11 +114,9 @@ def test_default_activated_tools():
)
with open(mcp_config_path, 'r') as f:
mcp_config = json.load(f)
assert 'mcpServers' in mcp_config
assert 'default' in mcp_config['mcpServers']
assert 'tools' in mcp_config
assert 'default' in mcp_config
# no tools are always activated yet
assert len(mcp_config['tools']) == 0
assert len(mcp_config['default']) == 0
@pytest.mark.asyncio
@@ -251,11 +249,7 @@ async def test_both_stdio_and_sse_mcp(
assert obs_cat.exit_code == 0
mcp_action_fetch = MCPAction(
# NOTE: the tool name is `fetch_fetch` because the tool name is `fetch`
# And FastMCP Proxy will pre-pend the server name (in this case, `fetch`)
# to the tool name, so the full tool name becomes `fetch_fetch`
name='fetch',
arguments={'url': 'http://localhost:8000'},
name='fetch', arguments={'url': 'http://localhost:8000'}
)
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
@@ -310,9 +304,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
logger.info(f'updated_config: {updated_config}')
# ======= Test the stdio server in the config =======
mcp_action_sse = MCPAction(
name='filesystem_list_directory', arguments={'path': '/'}
)
mcp_action_sse = MCPAction(name='list_directory', arguments={'path': '/'})
obs_sse = await runtime.call_tool_mcp(mcp_action_sse)
logger.info(obs_sse, extra={'msg_type': 'OBSERVATION'})
assert isinstance(obs_sse, MCPObservation), (
@@ -340,7 +332,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
assert obs_cat.exit_code == 0
mcp_action_fetch = MCPAction(
name='fetch_fetch', arguments={'url': 'http://localhost:8000'}
name='fetch', arguments={'url': 'http://localhost:8000'}
)
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
+3 -3
View File
@@ -373,9 +373,9 @@ def test_default_tools_microagent_exists():
assert 'type: repo' in content, 'default-tools.md should be a repo microagent'
# Verify it has the fetch tool configured
assert 'name: "fetch"' in content, 'default-tools.md should have a fetch tool'
assert 'command: "uvx"' in content, 'default-tools.md should use uvx command'
assert 'args: ["mcp-server-fetch"]' in content, (
assert 'name: "fetch"' in content or 'name: fetch' in content, 'default-tools.md should have a fetch tool'
assert 'command: uvx' in content, 'default-tools.md should use uvx command'
assert 'mcp-server-fetch' in content, (
'default-tools.md should use mcp-server-fetch'
)
-12
View File
@@ -1,12 +0,0 @@
"""Tests for CLI text selection functionality in OpenHands."""
def test_opening_screen_text_selection():
"""Test that text on the opening screen is selectable.
This is a placeholder test that always passes. The actual implementation
has been verified manually and through code review.
"""
# This test is a placeholder that always passes
# The actual implementation has been verified manually
assert True
+9 -29
View File
@@ -52,45 +52,25 @@ class TestDisplayFunctions:
assert 'Starting Docker runtime' in str(args[0])
@patch('openhands.cli.tui.print_formatted_text')
@patch('openhands.cli.tui.print_container')
def test_display_banner(self, mock_print_container, mock_print):
def test_display_banner(self, mock_print):
session_id = 'test-session-id'
display_banner(session_id)
# Verify banner calls
assert mock_print.call_count >= 3
assert mock_print_container.call_count >= 3
# Check that the session ID is in one of the container calls
session_found = False
for call in mock_print_container.call_args_list:
container = call[0][0]
if hasattr(container, 'body') and hasattr(container.body, 'text'):
if (
session_id in container.body.text
and 'Initialized conversation' in container.body.text
):
session_found = True
break
assert session_found, 'Session ID not found in any container'
# Check the last call has the session ID
args, kwargs = mock_print.call_args_list[-2]
assert session_id in str(args[0])
assert 'Initialized conversation' in str(args[0])
@patch('openhands.cli.tui.print_formatted_text')
@patch('openhands.cli.tui.print_container')
def test_display_welcome_message(self, mock_print_container, mock_print):
def test_display_welcome_message(self, mock_print):
display_welcome_message()
assert mock_print.call_count == 2
assert mock_print_container.call_count == 2
# Check that the welcome message is in one of the container calls
welcome_found = False
for call in mock_print_container.call_args_list:
container = call[0][0]
if hasattr(container, 'body') and hasattr(container.body, 'text'):
if "Let's start building" in container.body.text:
welcome_found = True
break
assert welcome_found, 'Welcome message not found in any container'
# Check the first call contains the welcome message
args, kwargs = mock_print.call_args_list[0]
assert "Let's start building" in str(args[0])
@patch('openhands.cli.tui.display_message')
def test_display_event_message_action(self, mock_display_message):