├── .DS_Store
├── .gitignore
├── .gitmodules
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── deepseek-planner
├── .env.example
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
├── src
│ └── server.py
└── uv.lock
├── dify_mcp_servers
├── README.md
├── cli.txt
├── dify_mcp_server.py
└── weather.py
├── gamelift-mcp-server
├── .gitignore
├── .python-version
├── Dockerfile
├── README.md
├── main.py
├── pyproject.toml
└── src
│ └── gamelift_mcp_server.py
├── html_render_service
├── .DS_Store
├── .gitignore
├── .python-version
├── LICENSE
├── README.md
├── asset
│ ├── case_3_1.png
│ └── case_3_2.png
├── pyproject.toml
├── src
│ └── server.py
├── uv.lock
└── web
│ ├── Dockerfile
│ ├── app
│ ├── __init__.py
│ ├── extensions
│ │ ├── __init__.py
│ │ ├── checkbox.py
│ │ ├── radio.py
│ │ └── textbox.py
│ └── static
│ │ ├── app.js
│ │ ├── base.html
│ │ ├── sample-quiz-animation.gif
│ │ ├── sample-quiz-md-file.PNG
│ │ └── wrapper.html
│ ├── docker-compose.yml
│ ├── main.py
│ └── requirements.txt
├── remote_computer_use
├── .env.example
├── .gitignore
├── .python-version
├── INSTALL.md
├── README.md
├── assets
│ └── image1.png
├── docker
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yml
│ ├── start_vnc.sh
│ └── xstartup
├── pyproject.toml
├── server.py
├── server_claude.py
├── setup_vnc.sh
├── ssh_controller.py
├── test_connection.py
├── tools
│ ├── base.py
│ ├── bash.py
│ ├── computer.py
│ ├── edit.py
│ └── tools_config.py
├── uv.lock
└── vnc_controller.py
├── s3_upload_server
├── .python-version
├── README.md
├── pyproject.toml
├── src
│ └── server.py
├── test_server.py
└── uv.lock
├── streamble_mcp_server_demo
├── .gitignore
├── .python-version
├── README.md
├── main.py
├── pyproject.toml
├── setup.sh
├── src
│ ├── auth.py
│ ├── cognito_auth.py
│ └── server.py
└── uv.lock
└── time_server
├── .python-version
├── README.md
├── hello.py
├── pyproject.toml
├── src
└── server.py
└── uv.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be added to the global gitignore or merged into this project gitignore. For a PyCharm
158 | # project, it is recommended to include the .idea directory in version control.
159 | # https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this project gitignore. For a PyCharm
161 | # project, it is recommended to include the .idea directory in version control.
162 | .idea/
163 |
164 | # VS Code
165 | .vscode/
166 |
167 | # macOS
168 | .DS_Store
169 | .DS_Store?
170 | ._*
171 | .Spotlight-V100
172 | .Trashes
173 | ehthumbs.db
174 | Thumbs.db
175 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/.gitmodules
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT No Attribution
2 |
3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sample MCP Servers for AWS GCR
2 |
3 | ## Contributed by AWS GCR
4 |
5 | | 序号 | 名称 | 描述 | 作者 | 链接 |
6 | |------|------|------|------|------|
7 | | 1 | Remote Computer Use | 使用MCP连接ubuntu virtual desktop作为computer use sandbox使用 | chuanxie@ | [Computer Use](remote_computer_use/README.md) |
8 | | 2 | Dify worflow mcp server demo | 使用Python MCP SDK 实现 Dify workflow mcp serverr | lht@ | [Dify worflow mcp server demo](https://github.com/aws-samples/aws-mcp-servers-samples/blob/main/dify_mcp_servers/README.md) |
9 | | 3 | Deepseek planner | 使用Bedrock上DeepSeek R1做planning, coding | chuanxie@ | [deepseek-planner](deepseek-planner/README.md) |
10 | | 4 | Time Server | 让Agent知道当前实际时间 | chuanxie@ | [time-server](time_server/README.md) |
11 | | 5 | Html Render Service | 把Markdown文件或者HTML转成网页渲染出来 | chuanxie@ | [Html-Render-Service](html_render_service/README.md) |
12 | | 6 | GameLift MCP Server | 使用MCP协议来获取当前账户的GameLift相关信息 | seanguo@ yuzp@ | [gamelift-mcp-server](gamelift-mcp-server/README.md) |
13 | | 7 | S3 Upload Server | 上传文件到S3并返回公共访问链接的MCP服务器 | hcihang@ | [s3-upload-server](s3_upload_server/README.md) |
14 |
15 | ## Demo MCP on Amazon Bedrock
16 | 推荐Bedrock MCP Demo:
17 | https://github.com/aws-samples/demo_mcp_on_amazon_bedrock
18 |
--------------------------------------------------------------------------------
/deepseek-planner/.env.example:
--------------------------------------------------------------------------------
1 | # AWS Credentials for Amazon Bedrock
2 | AWS_ACCESS_KEY_ID=your_access_key_id
3 | AWS_SECRET_ACCESS_KEY=your_secret_access_key
4 | AWS_REGION=us-east-1
5 |
6 | # Optional: AWS Session Token (if using temporary credentials)
7 | # AWS_SESSION_TOKEN=your_session_token
8 |
--------------------------------------------------------------------------------
/deepseek-planner/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | venv/
8 | ENV/
9 | env/
10 | .venv
11 | .env
12 | *.egg-info/
13 | dist/
14 | build/
15 |
16 | # IDE files
17 | .vscode/
18 | .idea/
19 | *.swp
20 | *.swo
21 |
22 | # Logs
23 | *.log
24 |
25 | # OS files
26 | .DS_Store
27 | Thumbs.db
28 |
--------------------------------------------------------------------------------
/deepseek-planner/.python-version:
--------------------------------------------------------------------------------
1 | 3.11
2 |
--------------------------------------------------------------------------------
/deepseek-planner/README.md:
--------------------------------------------------------------------------------
1 | # DeepSeek Planner MCP Server
2 |
3 | An MCP (Model Context Protocol) server that provides planning and coding assistance using the DeepSeek model hosted on Amazon Bedrock.
4 |
5 | ## Features
6 |
7 | - **Project Planning**: Generate detailed project plans based on requirements
8 | - **Code Generation**: Create code in various programming languages
9 | - **Code Review**: Get feedback on your code
10 | - **Code Explanation**: Understand complex code
11 | - **Code Refactoring**: Improve your code quality
12 |
13 | ## Prerequisites
14 |
15 | - Python 3.8 or higher
16 | - AWS account with access to Amazon Bedrock
17 | - AWS credentials with permissions to invoke the DeepSeek model
18 |
19 | ## Installation
20 |
21 | 1. Clone this repository
22 | 2. Create and activate a virtual environment:
23 | ```
24 | uv sync
25 | source venv/bin/activate
26 | ```
27 |
28 | ## Usage
29 |
30 | ### Setup
31 | 1. Add the DeepSeek Planner server configuration:
32 | ```json
33 | {
34 | "mcpServers": {
35 | "deepseek-planner": {
36 | "command": "uv",
37 | "args": [
38 | "--directory",
39 | "/path/to/deepseek-planner/src",
40 | "run",
41 | "server.py"
42 | ],
43 | "env": {
44 | "AWS_ACCESS_KEY_ID": "your_access_key_id",
45 | "AWS_SECRET_ACCESS_KEY": "your_secret_access_key",
46 | "AWS_REGION": "us-east-1"
47 | }
48 | }
49 | }
50 | }
51 | ```
52 |
53 | ### Available Tools
54 |
55 | 1. **generate_plan**: Create a detailed project plan based on requirements
56 | 2. **generate_code**: Generate code based on requirements
57 | 3. **review_code**: Review code and provide feedback
58 | 4. **explain_code**: Explain code in detail
59 | 5. **refactor_code**: Refactor code to improve quality
60 |
61 | ## License
62 |
63 | MIT
64 |
--------------------------------------------------------------------------------
/deepseek-planner/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "deepseek-planner"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.11"
7 | dependencies = [
8 | "boto3>=1.37.18",
9 | "mcp[cli]>=1.5.0",
10 | "python-dotenv>=1.0.1",
11 | ]
12 |
--------------------------------------------------------------------------------
/deepseek-planner/src/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import json
4 | import boto3
5 | from typing import Optional, Dict, Any, List
6 | from mcp.server.fastmcp import FastMCP, Context
7 | from botocore.client import Config
8 | # import dotenv
9 | # dotenv.load_dotenv()
10 |
11 | # Initialize FastMCP server
12 | mcp = FastMCP("deepseek-planner")
13 | custom_config = Config(connect_timeout=840, read_timeout=840)
14 | MAX_TOKENS = int(os.environ.get("MAX_TOKENS", 16000))
15 | # Initialize AWS Bedrock client
16 | bedrock_runtime = boto3.client(
17 | service_name="bedrock-runtime",
18 | region_name=os.environ.get("AWS_REGION", "us-east-1"),
19 | aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
20 | aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
21 | aws_session_token=os.environ.get("AWS_SESSION_TOKEN"), # Optional
22 | config=custom_config,
23 | )
24 |
25 | # DeepSeek model ID on Bedrock
26 | DEEPSEEK_MODEL_ID = "us.deepseek.r1-v1:0"
27 |
28 | def invoke_deepseek(messages: List[Dict[str, str]],
29 | temperature: float = 0.7,
30 | max_tokens: int = 2048
31 | ) -> str:
32 | """
33 | Invoke the DeepSeek model via AWS Bedrock using the converse API
34 | """
35 | try:
36 | # Prepare the request body
37 | body = {
38 | "modelId": DEEPSEEK_MODEL_ID,
39 | "messages": messages[1:],
40 | "system": messages[0]['content'],
41 | "inferenceConfig": {
42 | "maxTokens": max_tokens,
43 | "temperature": temperature,
44 | }
45 | }
46 |
47 | response = bedrock_runtime.converse(
48 | **body
49 | )
50 |
51 | # Parse the response
52 | text = [content["text"] for content in response["output"]["message"]["content"] if "text" in content][0]
53 | return text
54 |
55 | except Exception as e:
56 | print(f"Error invoking DeepSeek model: {e}")
57 | raise
58 |
59 | @mcp.tool()
60 | async def generate_plan(requirements: str,
61 | context: Optional[str] = None,
62 | format: str = "markdown") -> str:
63 | """Generate a detailed project plan based on requirements.
64 |
65 | Args:
66 | requirements: Project requirements and goals
67 | context: Additional context or constraints (optional)
68 | format: Output format (markdown, json, or text)
69 | """
70 | try:
71 | # Prepare messages for DeepSeek
72 | messages = [
73 | {
74 | "role": "system",
75 | "content": [{"text":"You are an expert project planner. Your task is to create detailed, actionable project plans based on requirements. Be thorough, practical, and consider all aspects of project planning including timeline, resources, milestones, and potential challenges."}]
76 | },
77 | {
78 | "role": "user",
79 | "content": [{"text":f"Please create a detailed project plan for the following requirements:\n\n{requirements}\n\n" + f'Additional context: {context}\n\n' if context else '' + f"Please provide the plan in {format} format."}]
80 | }
81 | ]
82 |
83 | # Invoke DeepSeek model
84 | response = invoke_deepseek(
85 | messages=messages,
86 | temperature=0.7,
87 | max_tokens=MAX_TOKENS,
88 | )
89 |
90 | return response
91 |
92 | except Exception as e:
93 | return f"Error generating plan: {str(e)}"
94 |
95 | @mcp.tool()
96 | async def generate_code(language: str,
97 | task: str,
98 | context: Optional[str] = None,
99 | comments: bool = True) -> str:
100 | """Generate code based on requirements.
101 |
102 | Args:
103 | language: Programming language
104 | task: Description of what the code should do
105 | context: Additional context or existing code (optional)
106 | comments: Whether to include comments in the code
107 | """
108 | try:
109 | # Prepare messages for DeepSeek
110 | messages = [
111 | {
112 | "role": "system",
113 | "content": [{"text":"You are an expert programmer. Your task is to generate high-quality, efficient, and well-structured code based on requirements. Follow best practices for the specified programming language."}]
114 | },
115 | {
116 | "role": "user",
117 | "content": [{"text":f"Please generate {language} code for the following task:\n\n{task}\n\n" + f'Additional context or existing code:\n```{language}\n{context}\n```\n\n' if context else '' + 'Please include detailed comments.' if comments else 'No need for extensive comments.'}]
118 | }
119 | ]
120 |
121 | # Invoke DeepSeek model
122 | response = invoke_deepseek(
123 | messages=messages,
124 | temperature=0.3, # Lower temperature for code generation
125 | max_tokens=MAX_TOKENS,
126 | )
127 |
128 | return response
129 |
130 | except Exception as e:
131 | return f"Error generating code: {str(e)}"
132 |
133 | @mcp.tool()
134 | async def review_code(language: str,
135 | code: str,
136 | focus: Optional[List[str]] = None) -> str:
137 | """Review code and provide feedback.
138 |
139 | Args:
140 | language: Programming language
141 | code: Code to review
142 | focus: Areas to focus on (bugs, performance, security, style, architecture)
143 | """
144 | try:
145 | # Prepare messages for DeepSeek
146 | messages = [
147 | {
148 | "role": "system",
149 | "content": [{"text":"You are an expert code reviewer. Your task is to provide detailed, constructive feedback on code. Focus on identifying issues, suggesting improvements, and explaining your reasoning."}]
150 | },
151 | {
152 | "role": "user",
153 | "content": [{"text":f"Please review the following {language} code:\n\n```{language}\n{code}\n```\n\n" + f'Please focus on these aspects: {", ".join(focus)}' if focus else 'Please provide a comprehensive review.'}]
154 | }
155 | ]
156 |
157 | # Invoke DeepSeek model
158 | response = invoke_deepseek(
159 | messages=messages,
160 | temperature=0.5,
161 | max_tokens=MAX_TOKENS,
162 | )
163 |
164 | return response
165 |
166 | except Exception as e:
167 | return f"Error reviewing code: {str(e)}"
168 |
169 | @mcp.tool()
170 | async def explain_code(language: str,
171 | code: str,
172 | detail_level: str = "intermediate") -> str:
173 | """Explain code in detail.
174 |
175 | Args:
176 | language: Programming language
177 | code: Code to explain
178 | detail_level: Level of detail in the explanation (basic, intermediate, advanced)
179 | """
180 | try:
181 | # Prepare messages for DeepSeek
182 | messages = [
183 | {
184 | "role": "system",
185 | "content": [{"text":"You are an expert programmer and educator. Your task is to explain code clearly and accurately, adapting your explanation to the requested level of detail."}]
186 | },
187 | {
188 | "role": "user",
189 | "content": [{"text":f"Please explain the following {language} code at a {detail_level} level of detail:\n\n```{language}\n{code}\n```"}]
190 | }
191 | ]
192 |
193 | # Invoke DeepSeek model
194 | response = invoke_deepseek(
195 | messages=messages,
196 | temperature=0.5,
197 | max_tokens=MAX_TOKENS,
198 | )
199 |
200 | return response
201 |
202 | except Exception as e:
203 | return f"Error explaining code: {str(e)}"
204 |
205 | @mcp.tool()
206 | async def refactor_code(language: str,
207 | code: str,
208 | goals: List[str]) -> str:
209 | """Refactor code to improve quality.
210 |
211 | Args:
212 | language: Programming language
213 | code: Code to refactor
214 | goals: Refactoring goals (readability, performance, modularity, security, maintainability)
215 | """
216 | try:
217 | # Prepare messages for DeepSeek
218 | messages = [
219 | {
220 | "role": "system",
221 | "content": [{"text":"You are an expert programmer specializing in code refactoring. Your task is to improve code quality while maintaining functionality. Provide both the refactored code and an explanation of your changes."}]
222 | },
223 | {
224 | "role": "user",
225 | "content": [{"text":f"Please refactor the following {language} code to improve {', '.join(goals)}:\n\n```{language}\n{code}\n```\n\nProvide the refactored code and explain your changes."}]
226 | }
227 | ]
228 |
229 | # Invoke DeepSeek model
230 | response = invoke_deepseek(
231 | messages=messages,
232 | temperature=0.4,
233 | max_tokens=MAX_TOKENS,
234 | )
235 |
236 | return response
237 |
238 | except Exception as e:
239 | return f"Error refactoring code: {str(e)}"
240 |
241 | if __name__ == "__main__":
242 | # Check if AWS credentials are set
243 | if not os.environ.get("AWS_ACCESS_KEY_ID") or not os.environ.get("AWS_SECRET_ACCESS_KEY"):
244 | print("AWS credentials are not set. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.")
245 | exit(1)
246 |
247 | # Run the server
248 | mcp.run(transport='stdio')
249 |
250 | # messages = [
251 | # {
252 | # "role": "system",
253 | # "content": [{"text":"You are an expert project planner. Your task is to create detailed, actionable project plans based on requirements. Be thorough, practical, and consider all aspects of project planning including timeline, resources, milestones, and potential challenges."}]
254 | # },
255 | # {
256 | # "role": "user",
257 | # "content": [{"text":f"Please create a detailed project plan for the following requirements:\n\n帮我制作一份司美格鲁肽的介绍,包括特色功能,适用范围,发展历史,价格,图文并茂,需要制作成精美的 HTML保存到本地目录. "}]
258 | # }
259 | # ]
260 |
261 | # response = invoke_deepseek(
262 | # messages=messages,
263 | # temperature=0.5,
264 | # max_tokens=MAX_TOKENS,
265 | # )
266 |
267 | # print(response)
--------------------------------------------------------------------------------
/dify_mcp_servers/README.md:
--------------------------------------------------------------------------------
1 | # Dify MCP Servers Project
2 |
3 | An agent developed specifically for Dify, implementing an MCP server. You can integrate your Dify workflow or Dify chat workflow with MCP.
4 |
5 | ## Development Process
6 |
7 | 1. Log in to Dify, select the workflow you want to integrate, check the API address, and create a new API_KEY.
8 | 2. Test the interface using CLI, referring to the Workflow parameter settings. The parameters in the inputs are the workflow inputs:
9 | ```bash
10 | curl -X POST 'https://api.dify.ai/v1/workflows/run' \
11 | --header 'Authorization: Bearer api-key' \
12 | --header 'Content-Type: application/json' \
13 | --data-raw '{
14 | "inputs": {"ad_data": "你好,请介绍一下自己"},
15 | "response_mode": "streaming",
16 | "user": "abc-123"
17 | }'
18 | ```
19 | 3. Use Amazon Q CLI with the prompt: "The cli.txt file in the project contains a runnable HTTP API request example, please refer to this to generate an MCP server similar to weather.py"
20 | 4. weather.py is a sample provided by the MCP official website, so we can reference it to generate our own, or implement it with custom code.
21 |
22 | ## Setup Instructions
23 |
24 | ### Dify MCP Server
25 |
26 | 1. Install the required dependencies:
27 | ```bash
28 | curl -LsSf https://astral.sh/uv/install.sh | sh
29 | pip install -r dify_mcp_server/requirements.txt
30 | ```
31 |
32 | 2. Run the server:
33 | ```bash
34 | uv init
35 | uv venv
36 | source .venv/bin/activate
37 | uv add "mcp[cli]" httpx
38 | uv run dify_mcp_server/dify_mcp_server.py
39 | ```
40 |
41 | 3. Configure MCP Server:
42 | ```json
43 | "ad_delivery_data_analysis": {
44 | "command": "uv",
45 | "args": [
46 | "--directory",
47 | "/Users/lht/Documents/GitHub/dify_mcp_server",
48 | "run",
49 | "dify_mcp_server.py"
50 | ],
51 | "description": "Analysis advertisement delivery data, get insights, and provide advice",
52 | "status": 1
53 | }
54 | ```
55 |
56 | ## Usage
57 |
58 | To use an MCP server, you need to connect to it and use the tools and resources it provides. Refer to the documentation of each server for more information on how to use it.
59 |
--------------------------------------------------------------------------------
/dify_mcp_servers/cli.txt:
--------------------------------------------------------------------------------
1 | curl -X POST 'https://api.dify.ai/v1/workflows/run' \
2 | --header 'Authorization: Bearer api-key' \
3 | --header 'Content-Type: application/json' \
4 | --data-raw '{
5 | "inputs": {"ad_data": "你好,请介绍一下自己"},
6 | "response_mode": "streaming",
7 | "user": "abc-123"
8 | }'
--------------------------------------------------------------------------------
/dify_mcp_servers/dify_mcp_server.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional
2 | import httpx
3 | import json
4 | from mcp.server.fastmcp import FastMCP
5 |
6 | # Initialize FastMCP server
7 | mcp = FastMCP("dify")
8 |
9 | # Constants
10 | DIFY_API_BASE = "https://api.dify.ai/v1" # You can get it from dify console.
11 | DEFAULT_API_KEY = "API_KEY " # You can get the API_KEY from dify console.
12 |
13 | async def make_dify_request(endpoint: str, data: Dict[str, Any], api_key: str = DEFAULT_API_KEY, streaming: bool = True) -> Dict[str, Any]:
14 | """Make a request to the Dify API with proper error handling.
15 |
16 | Args:
17 | endpoint: API endpoint to call
18 | data: Request payload
19 | api_key: Dify API key
20 | streaming: Whether to use streaming response mode
21 | """
22 | headers = {
23 | "Authorization": f"Bearer {api_key}",
24 | "Content-Type": "application/json"
25 | }
26 |
27 | # Set response mode based on streaming parameter
28 | if "response_mode" not in data:
29 | data["response_mode"] = "streaming" if streaming else "blocking"
30 |
31 | url = f"{DIFY_API_BASE}/{endpoint}"
32 |
33 | async with httpx.AsyncClient() as client:
34 | try:
35 | response = await client.post(url, headers=headers, json=data, timeout=60.0)
36 | response.raise_for_status()
37 | return response.json()
38 | except httpx.HTTPStatusError as e:
39 | return {"error": f"HTTP error: {e.response.status_code}", "details": e.response.text}
40 | except Exception as e:
41 | return {"error": f"Request failed: {str(e)}"}
42 |
43 | @mcp.tool()
44 | async def run_workflow(inputs: Dict[str, str], user_id: Optional[str] = "abc-123", api_key: Optional[str] = None) -> str:
45 | """Run a Dify workflow with the provided inputs.
46 |
47 | Args:
48 | inputs: Dictionary of input parameters for the workflow
49 | user_id: Optional user identifier for the request
50 | api_key: Optional API key to override the default
51 | """
52 | data = {
53 | "inputs": inputs,
54 | "response_mode": "blocking", # Using blocking for MCP tool response
55 | "user": user_id
56 | }
57 |
58 | result = await make_dify_request("workflows/run", data, api_key or DEFAULT_API_KEY, streaming=False)
59 |
60 | if "error" in result:
61 | return f"Error: {result['error']}\n{result.get('details', '')}"
62 |
63 | # You shoud replace the output processing with your dify workflow input
64 | try:
65 | if "data" in result and "outputs" in result["data"] and "advice" in result["data"]["outputs"]:
66 | advice = result["data"]["outputs"]["advice"]
67 | return advice
68 | else:
69 | return "No advice found in the response."
70 | except Exception as e:
71 | return f"Failed to parse response: {str(e)}"
72 |
73 | @mcp.tool()
74 | async def chat_completion(message: str, conversation_id: Optional[str] = None,
75 | user_id: Optional[str] = None, api_key: Optional[str] = None) -> str:
76 | """Send a message to Dify chat completion API.
77 |
78 | Args:
79 | message: The user message to send
80 | conversation_id: Optional conversation ID for continuing a conversation
81 | user_id: Optional user identifier for the request
82 | api_key: Optional API key to override the default
83 | """
84 | data = {
85 | "inputs": {},
86 | "query": message,
87 | "response_mode": "blocking" # Using blocking for MCP tool response
88 | }
89 |
90 | if user_id:
91 | data["user"] = user_id
92 |
93 | if conversation_id:
94 | data["conversation_id"] = conversation_id
95 |
96 | result = await make_dify_request("chat-messages", data, api_key or DEFAULT_API_KEY, streaming=False)
97 |
98 | if "error" in result:
99 | return f"Error: {result['error']}\n{result.get('details', '')}"
100 |
101 | # Extract only the answer from the response
102 | try:
103 | answer = result.get("answer", "No answer provided")
104 | return answer
105 | except Exception as e:
106 | return f"Failed to parse response: {str(e)}"
107 |
108 | @mcp.tool()
109 | async def get_conversation_history(conversation_id: str, first_id: Optional[str] = None,
110 | limit: int = 20, api_key: Optional[str] = None) -> str:
111 | """Retrieve conversation history from Dify.
112 |
113 | Args:
114 | conversation_id: The ID of the conversation to retrieve
115 | first_id: Optional first message ID for pagination
116 | limit: Maximum number of messages to retrieve (default: 20)
117 | api_key: Optional API key to override the default
118 | """
119 | endpoint = f"conversations/{conversation_id}/messages"
120 | params = {}
121 |
122 | if first_id:
123 | params["first_id"] = first_id
124 |
125 | if limit:
126 | params["limit"] = limit
127 |
128 | headers = {
129 | "Authorization": f"Bearer {api_key or DEFAULT_API_KEY}",
130 | "Content-Type": "application/json"
131 | }
132 |
133 | url = f"{DIFY_API_BASE}/{endpoint}"
134 |
135 | async with httpx.AsyncClient() as client:
136 | try:
137 | response = await client.get(url, headers=headers, params=params, timeout=30.0)
138 | response.raise_for_status()
139 | result = response.json()
140 |
141 | if "data" not in result:
142 | return "No conversation history found or error retrieving history."
143 |
144 | messages = result["data"]
145 | formatted_messages = []
146 |
147 | for msg in messages:
148 | role = msg.get("role", "unknown")
149 | content = msg.get("content", "No content")
150 | formatted_messages.append(f"{role.upper()}: {content}")
151 |
152 | return "\n\n".join(formatted_messages)
153 | except Exception as e:
154 | return f"Failed to retrieve conversation history: {str(e)}"
155 |
156 | if __name__ == "__main__":
157 | # Initialize and run the server
158 | mcp.run(transport='stdio')
159 |
--------------------------------------------------------------------------------
/dify_mcp_servers/weather.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | import httpx
3 | from mcp.server.fastmcp import FastMCP
4 |
5 | # Initialize FastMCP server
6 | mcp = FastMCP("weather")
7 |
8 | # Constants
9 | NWS_API_BASE = "https://api.weather.gov"
10 | USER_AGENT = "weather-app/1.0"
11 |
12 | async def make_nws_request(url: str) -> dict[str, Any] | None:
13 | """Make a request to the NWS API with proper error handling."""
14 | headers = {
15 | "User-Agent": USER_AGENT,
16 | "Accept": "application/geo+json"
17 | }
18 | async with httpx.AsyncClient() as client:
19 | try:
20 | response = await client.get(url, headers=headers, timeout=30.0)
21 | response.raise_for_status()
22 | return response.json()
23 | except Exception:
24 | return None
25 |
26 | def format_alert(feature: dict) -> str:
27 | """Format an alert feature into a readable string."""
28 | props = feature["properties"]
29 | return f"""
30 | Event: {props.get('event', 'Unknown')}
31 | Area: {props.get('areaDesc', 'Unknown')}
32 | Severity: {props.get('severity', 'Unknown')}
33 | Description: {props.get('description', 'No description available')}
34 | Instructions: {props.get('instruction', 'No specific instructions provided')}
35 | """
36 |
37 | @mcp.tool()
38 | async def get_alerts(state: str) -> str:
39 | """Get weather alerts for a US state.
40 |
41 | Args:
42 | state: Two-letter US state code (e.g. CA, NY)
43 | """
44 | url = f"{NWS_API_BASE}/alerts/active/area/{state}"
45 | data = await make_nws_request(url)
46 |
47 | if not data or "features" not in data:
48 | return "Unable to fetch alerts or no alerts found."
49 |
50 | if not data["features"]:
51 | return "No active alerts for this state."
52 |
53 | alerts = [format_alert(feature) for feature in data["features"]]
54 | return "\n---\n".join(alerts)
55 |
56 | @mcp.tool()
57 | async def get_forecast(latitude: float, longitude: float) -> str:
58 | """Get weather forecast for a location.
59 |
60 | Args:
61 | latitude: Latitude of the location
62 | longitude: Longitude of the location
63 | """
64 | # First get the forecast grid endpoint
65 | points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
66 | points_data = await make_nws_request(points_url)
67 |
68 | if not points_data:
69 | return "Unable to fetch forecast data for this location."
70 |
71 | # Get the forecast URL from the points response
72 | forecast_url = points_data["properties"]["forecast"]
73 | forecast_data = await make_nws_request(forecast_url)
74 |
75 | if not forecast_data:
76 | return "Unable to fetch detailed forecast."
77 |
78 | # Format the periods into a readable forecast
79 | periods = forecast_data["properties"]["periods"]
80 | forecasts = []
81 | for period in periods[:5]: # Only show next 5 periods
82 | forecast = f"""
83 | {period['name']}:
84 | Temperature: {period['temperature']}°{period['temperatureUnit']}
85 | Wind: {period['windSpeed']} {period['windDirection']}
86 | Forecast: {period['detailedForecast']}
87 | """
88 | forecasts.append(forecast)
89 |
90 | return "\n---\n".join(forecasts)
91 |
92 |
93 | if __name__ == "__main__":
94 | # Initialize and run the server
95 | mcp.run(transport='stdio')
--------------------------------------------------------------------------------
/gamelift-mcp-server/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | .env
25 | .venv
26 | env/
27 | venv/
28 | ENV/
29 |
30 | # IDE
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | .DS_Store
36 |
37 | # Project specific
38 | uv.lock
39 |
--------------------------------------------------------------------------------
/gamelift-mcp-server/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/gamelift-mcp-server/Dockerfile:
--------------------------------------------------------------------------------
1 | # 基于官方Python 3.12镜像
2 | FROM python:3.12-slim
3 |
4 | # Set environment variables
5 | ENV PYTHONDONTWRITEBYTECODE=1
6 | ENV PYTHONUNBUFFERED=1
7 |
8 | # 安装uv(推荐的Python包管理工具)
9 | RUN pip install --upgrade pip \
10 | && pip install uv
11 |
12 | # 设置工作目录
13 | WORKDIR /app
14 |
15 | # 复制项目文件
16 | COPY . /app
17 |
18 | # 安装依赖
19 | RUN uv pip install --system --no-cache-dir .
20 |
21 | # 设置环境变量(可根据需要覆盖)
22 | ENV AWS_ACCESS_KEY_ID=xxxxx \
23 | AWS_SECRET_ACCESS_KEY=xxxxxx \
24 | AWS_REGION=us-east-1
25 |
26 | # 默认启动命令
27 | CMD ["uv", "run", "src/gamelift_mcp_server.py"]
--------------------------------------------------------------------------------
/gamelift-mcp-server/README.md:
--------------------------------------------------------------------------------
1 | # GameLift MCP Server
2 |
3 | This project provides a simple MCP server for managing AWS GameLift fleets and container fleets. It exposes several API endpoints for querying fleet information, attributes, and echo testing.
4 |
5 | ## Features
6 | - Query GameLift fleets in a specific AWS region
7 | - Query GameLift container fleets in a specific AWS region
8 | - Get detailed attributes for a given fleet or container fleet
9 | - Simple echo endpoint for testing
10 |
11 | ## Requirements
12 | - Python 3.12+
13 | - AWS credentials with GameLift permissions
14 | - The following Python packages:
15 | - boto3
16 | - httpx
17 | - mcp.server.fastmcp (custom or third-party)
18 |
19 | ## Environment Variables
20 | - `AWS_PROFILE`: Your AWS profile name (optional, if not set, will use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)
21 | - `AWS_ACCESS_KEY_ID`: Your AWS access key (required if AWS_PROFILE is not set)
22 | - `AWS_SECRET_ACCESS_KEY`: Your AWS secret key (required if AWS_PROFILE is not set)
23 |
24 | ## How to Run
25 | 1. Install dependencies:
26 | ```bash
27 | pip install boto3 httpx
28 | # Install mcp.server.fastmcp as required
29 | ```
30 | 2. Set AWS credentials in your environment (choose one method):
31 | ```bash
32 | # Method 1: Using AWS Profile
33 | export AWS_PROFILE=your_profile_name
34 |
35 | # Method 2: Using Access Keys
36 | export AWS_ACCESS_KEY_ID=your_access_key
37 | export AWS_SECRET_ACCESS_KEY=your_secret_key
38 | ```
39 | 3. Start the MCP server:
40 | ```bash
41 | python src/gamelift_mcp_server.py
42 | ```
43 |
44 | ## API Endpoints (Tools)
45 | - `get_game_lift_fleets(region: str = 'us-east-1') -> str`: List all GameLift fleets in the specified region.
46 | - `get_gamelift_container_fleets(region: str = 'us-east-1') -> str`: List all GameLift container fleets in the specified region.
47 | - `get_fleet_attributes(fleet_id: str, region: str = 'us-east-1') -> str`: Get attributes for a specific GameLift fleet.
48 | - `get_container_fleet_attributes(fleet_id: str, region: str = 'us-east-1') -> str`: Get attributes for a specific GameLift container fleet.
49 | - `get_compute_auth_token(fleet_id: str, region: str = 'us-east-1', compute_name: str = '') -> str`: Get compute auth token for an ANYWHERE fleet.
50 | - `get_vpc_peering_connections(fleet_id: str, region: str = 'us-east-1') -> str`: Get VPC peering connections for a specific fleet.
51 | - `get_builds(region: str = 'us-east-1') -> str`: List all GameLift builds in the specified region.
52 | - `get_fleet_capacity(fleet_id_list: List[str], region: str = 'us-east-1') -> str`: Get capacity information for a list of fleets (not supported for ANYWHERE fleets).
53 |
54 | ## Config Mcp Server
55 | ```
56 | {
57 | "mcpServers": {
58 | "gamelift_mcp_server": {
59 | "command": "uv",
60 | "args": [
61 | "--directory",
62 | "/path/to/gamelift-mcp-server/src",
63 | "run",
64 | "gamelift_mcp_server.py"
65 | ],
66 | "env": {
67 | "AWS_ACCESS_KEY_ID": "xxxx",
68 | "AWS_SECRET_ACCESS_KEY": "xxxxx",
69 | "AWS_REGION": "us-east-1"
70 | }
71 | }
72 | }
73 | }
74 | ```
75 |
76 |
77 | ## Notes
78 | - Make sure your AWS account has the necessary GameLift permissions.
79 | - The MCP server is designed for internal or development use.
80 |
81 | ---
82 |
83 | ## License
84 |
85 | This project is licensed under the MIT License.
86 |
87 | ```
88 | MIT License
89 |
90 | Copyright (c) 2024
91 |
92 | Permission is hereby granted, free of charge, to any person obtaining a copy
93 | of this software and associated documentation files (the "Software"), to deal
94 | in the Software without restriction, including without limitation the rights
95 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
96 | copies of the Software, and to permit persons to whom the Software is
97 | furnished to do so, subject to the following conditions:
98 |
99 | The above copyright notice and this permission notice shall be included in all
100 | copies or substantial portions of the Software.
101 |
102 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
103 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
104 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
105 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
106 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
107 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
108 | SOFTWARE.
109 | ```
110 |
--------------------------------------------------------------------------------
/gamelift-mcp-server/main.py:
--------------------------------------------------------------------------------
1 | def main():
2 | print("Hello from gamelift-mcp-server!")
3 |
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
--------------------------------------------------------------------------------
/gamelift-mcp-server/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "gamelift-mcp-server"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "boto3>=1.38.18",
9 | "httpx>=0.28.1",
10 | "mcp[cli]>=1.9.0",
11 | ]
12 |
--------------------------------------------------------------------------------
/gamelift-mcp-server/src/gamelift_mcp_server.py:
--------------------------------------------------------------------------------
1 | from mcp.server.fastmcp import FastMCP
2 | import httpx
3 | import boto3
4 | import os
5 | import logging
6 | from typing import List
7 |
8 | # create MCP server instance
9 | mcp = FastMCP("gamelift_mcp_server")
10 |
11 | logging.basicConfig(level=logging.INFO)
12 | logger = logging.getLogger("gamelift_mcp_server")
13 |
14 | def get_gamelift_client(region: str):
15 | """Get a GameLift client using either AWS_PROFILE or AWS credentials
16 |
17 | Args:
18 | region: AWS region name
19 | """
20 | if os.environ.get("AWS_PROFILE"):
21 | session = boto3.Session(profile_name=os.environ.get("AWS_PROFILE"))
22 | return session.client('gamelift', region_name=region)
23 | else:
24 | return boto3.client('gamelift', region_name=region,
25 | aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
26 | aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"))
27 |
28 | # define a tool function, expose to client
29 | # @mcp.tool()
30 | # async def echo(message: str) -> str:
31 | # return f"Echo from MCP server: {message}"
32 |
33 | @mcp.tool()
34 | async def get_game_lift_fleets(region: str = os.environ.get("AWS_REGION")) -> str:
35 | """Get gamelift fleet list in specific region
36 |
37 | Args:
38 | region: AWS region name, e.g. us-east-1, if not provided, use us-east-1 as default
39 | """
40 | client = get_gamelift_client(region)
41 |
42 | # 1. Get All Fleet ID
43 | fleet_ids = []
44 | response = client.list_fleets()
45 | fleet_ids.extend(response.get('FleetIds', []))
46 |
47 | # pagination
48 | next_token = response.get('NextToken')
49 | while next_token:
50 | response = client.list_fleets(NextToken=next_token)
51 | fleet_ids.extend(response.get('FleetIds', []))
52 | next_token = response.get('NextToken')
53 |
54 | logger.info(f"Found {len(fleet_ids)} Fleet")
55 |
56 | # 2. batch call describe_fleet_attributes to get detailed information (API has a limit on the number of fleets per request, usually up to 100)
57 | def chunks(lst, n):
58 | for i in range(0, len(lst), n):
59 | yield lst[i:i + n]
60 |
61 | fleet_details = []
62 | for chunk_ids in chunks(fleet_ids, 100):
63 | response = client.describe_fleet_attributes(FleetIds=chunk_ids)
64 | fleet_details.extend(response.get('FleetAttributes', []))
65 |
66 | return fleet_details
67 |
68 | @mcp.tool()
69 | async def get_gamelift_container_fleets(region: str = os.environ.get("AWS_REGION")) -> str:
70 | """Get gamelift container fleet list in specific region
71 |
72 | Args:
73 | region: AWS region name, e.g. us-east-1, if not provided, use us-east-1 as default
74 | """
75 | client = get_gamelift_client(region)
76 |
77 | response = client.list_container_fleets()
78 | return response.get('ContainerFleets', [])
79 |
80 |
81 | @mcp.tool()
82 | async def get_fleet_attributes(fleet_id: str, region: str = os.environ.get("AWS_REGION")) -> str:
83 | """Get fleet attributes by fleet id
84 |
85 | Args:
86 | fleet_id: Gamelift fleet id
87 | """
88 | client = get_gamelift_client(region)
89 |
90 | response = client.describe_fleet_attributes(FleetIds=[fleet_id])
91 | return response.get('FleetAttributes', [])
92 |
93 |
94 | @mcp.tool()
95 | async def get_container_fleet_attributes(fleet_id: str, region: str = os.environ.get("AWS_REGION")) -> str:
96 | """Get container fleet attributes by fleet id
97 |
98 | Args:
99 | fleet_id: Gamelift container fleet id
100 | """
101 | client = get_gamelift_client(region)
102 |
103 | response = client.describe_container_fleet(FleetId=fleet_id)
104 | return response.get('ContainerFleet', [])
105 |
106 |
107 | @mcp.tool()
108 | async def get_compute_auth_token(fleet_id: str, region: str = os.environ.get("AWS_REGION"), compute_name: str = '') -> str:
109 | """Get compute auth token by fleet id and compute name
110 |
111 | Args:
112 | fleet_id: Gamelift fleet id
113 | compute_name: compute name
114 | """
115 | client = get_gamelift_client(region)
116 |
117 | # 先获取fleet属性,判断是否为ANYWHERE Fleet
118 | attr_response = client.describe_fleet_attributes(FleetIds=[fleet_id])
119 | attrs = attr_response.get('FleetAttributes', [])
120 | if not attrs or attrs[0].get('ComputeType') != 'ANYWHERE':
121 | raise Exception('Only ANYWHERE Fleets support compute auth token.')
122 |
123 | response = client.get_compute_auth_token(FleetId=fleet_id, ComputeName=compute_name)
124 | return response.get('AuthToken', '')
125 |
126 |
127 | @mcp.tool()
128 | async def get_vpc_peering_connections(fleet_id: str, region: str = os.environ.get("AWS_REGION")) -> str:
129 | """Get vpc peering connections by fleet id
130 |
131 | Args:
132 | fleet_id: Gamelift fleet id
133 | """
134 | client = get_gamelift_client(region)
135 |
136 | connections = []
137 | next_token = None
138 | while True:
139 | if next_token:
140 | response = client.describe_vpc_peering_connections(FleetId=fleet_id, NextToken=next_token)
141 | else:
142 | response = client.describe_vpc_peering_connections(FleetId=fleet_id)
143 | connections.extend(response.get('VpcPeeringConnections', []))
144 | next_token = response.get('NextToken')
145 | if not next_token:
146 | break
147 | return connections
148 |
149 |
150 | @mcp.tool()
151 | async def get_builds(region: str = os.environ.get("AWS_REGION")) -> str:
152 | """Get builds by region
153 |
154 | Args:
155 | region: AWS region name, e.g. us-east-1, if not provided, use us-east-1 as default
156 | """
157 | client = get_gamelift_client(region)
158 |
159 | builds = []
160 | next_token = None
161 | while True:
162 | if next_token:
163 | response = client.list_builds(NextToken=next_token)
164 | else:
165 | response = client.list_builds()
166 | builds.extend(response.get('Builds', []))
167 | next_token = response.get('NextToken')
168 | if not next_token:
169 | break
170 | return builds
171 |
172 |
173 | @mcp.tool()
174 | async def get_fleet_capacity(fleet_id_list: List[str], region: str = os.environ.get("AWS_REGION")) -> str:
175 | """Get fleet capacity by fleet id
176 |
177 | Args:
178 | fleet_id: Gamelift fleet id
179 | """
180 | client = get_gamelift_client(region)
181 |
182 | # check fleet is not a ANYWHERE Fleet
183 | for fleet_id in fleet_id_list:
184 | attr_response = client.describe_fleet_attributes(FleetIds=[fleet_id])
185 | attrs = attr_response.get('FleetAttributes', [])
186 | if not attrs or attrs[0].get('ComputeType') == 'ANYWHERE':
187 | raise Exception('ANYWHERE Fleets do not support fleet capacity.')
188 |
189 | builds = []
190 | next_token = None
191 | while True:
192 | if next_token:
193 | response = client.describe_fleet_capacity(FleetIds=fleet_id_list, NextToken=next_token)
194 | else:
195 | response = client.describe_fleet_capacity(FleetIds=fleet_id_list)
196 | builds.extend(response.get('FleetCapacity', []))
197 | next_token = response.get('NextToken')
198 | if not next_token:
199 | break
200 | return builds
201 |
202 |
203 | # start MCP Server
204 | if __name__ == "__main__":
205 | mcp.run()
206 |
--------------------------------------------------------------------------------
/html_render_service/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/.DS_Store
--------------------------------------------------------------------------------
/html_render_service/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 | data
12 | files
--------------------------------------------------------------------------------
/html_render_service/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/html_render_service/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Osanda Deshan Nimalarathna
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/html_render_service/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 | MCP server 用于将Agent生成的html或者markdown内容渲染成Web Page,并且可以通过浏览器直接访问
3 |
4 | ## 部署方法
5 |
6 | ### Step 1:使用 Docker Compose 启动Flask Web Server
7 |
8 | 1. 启动服务:
9 |
10 | ```bash
11 | cd aws-mcp-servers-samples/html_render_service/web
12 | docker-compose up -d
13 | ```
14 |
15 | 2. 验证是否启动成功:
16 |
17 | ```bash
18 | curl http://127.0.0.1:5006/
19 | ```
20 |
21 | 如果返回:
22 | ```
23 | {
24 | "message": "ok"
25 | }
26 | ```
27 | 表示启动成功
28 |
29 |
30 | 3. 可选:停止服务命令:
31 | ```bash
32 | docker-compose down
33 | ```
34 |
35 | ### Step 2:在MCP Client中添加如下:
36 | ```json
37 | {"mcpServers":
38 | { "html_render_service":
39 | { "command": "uv",
40 | "args":
41 | ["--directory","/path/to/html_render_service/src",
42 | "run",
43 | "server.py"]
44 | }
45 | }
46 | }
47 | ```
48 |
49 | ## 示例
50 |
51 | **输入:**
52 |
53 | ```
54 | 请帮我制定从北京到上海的高铁5日游计划(5月1日-5日),要求:
55 | - 交通:往返高铁选早上出发(5.1)和晚上返程(5.5)
56 | - 必去:迪士尼全天(推荐3个最值得玩的项目+看烟花)
57 | - 推荐:3个上海经典景点(含外滩夜景)和1个特色街区
58 | - 住宿:迪士尼住周边酒店,市区住地铁站附近
59 | - 附:每日大致花费预估和景点预约提醒
60 | 需要制作成精美的 HTML,并使用html render service上传html
61 | ```
62 |
63 | - 从结果中找到链接
64 | 
65 |
66 | - 用浏览器打开链接
67 | 
68 |
--------------------------------------------------------------------------------
/html_render_service/asset/case_3_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/asset/case_3_1.png
--------------------------------------------------------------------------------
/html_render_service/asset/case_3_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/asset/case_3_2.png
--------------------------------------------------------------------------------
/html_render_service/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "flask-webserver"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "mcp[cli]>=1.6.0",
9 | "requests>=2.32.3",
10 | ]
11 |
--------------------------------------------------------------------------------
/html_render_service/src/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Flask Web Service Server
4 | It is a web server tool to generate and render web page from Markdown files and html files.
5 | """
6 | import os
7 | import asyncio
8 | from dataclasses import dataclass
9 | from contextlib import asynccontextmanager
10 | from typing import AsyncIterator, Optional, Dict, Any, List
11 | import io
12 | import json
13 | import requests
14 | from mcp.server.fastmcp import FastMCP, Image, Context
15 |
16 | ENDPOINT = os.environ.get("endpoint","http://127.0.0.1:5006")
17 |
18 | @dataclass
19 | class AppContext:
20 | """Application context for lifespan management"""
21 | ready_status: bool
22 |
23 | @asynccontextmanager
24 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
25 | """
26 | Application lifespan management context manager.
27 |
28 | Args:
29 | server: FastMCP server instance
30 |
31 | Yields:
32 | AppContext: Application context with ready_status
33 | """
34 | try:
35 | response = requests.get(f"{ENDPOINT}/")
36 | response.raise_for_status()
37 | yield AppContext(ready_status=True)
38 |
39 | except requests.exceptions.RequestException as e:
40 | raise ValueError(f"Error connecting to server: {e}")
41 |
42 | # Create MCP server
43 | mcp = FastMCP(
44 | "Flask Web Service Server",
45 | app_lifespan=app_lifespan,
46 | dependencies=['requests'])
47 |
48 | @mcp.tool()
49 | async def render_markdown(file_name:str, markdown_content: str) -> str:
50 | """
51 | uploads markdown it to a server.
52 |
53 | Args:
54 | file_name: Name of the markdown file to be rendered
55 | markdown_content: Markdown content to be rendered
56 |
57 | Returns:
58 | URL string to access the rendered HTML page
59 | """
60 | try:
61 | response = requests.post(f"{ENDPOINT}/upload_markdown", json={"file_name": file_name, "file_content": markdown_content})
62 | response.raise_for_status()
63 | return response.json()['url']
64 | except requests.exceptions.RequestException as e:
65 | raise ValueError(f"Error uploading HTML file: {e}")
66 |
67 | @mcp.tool()
68 | async def render_html(file_name:str, html_content: str) -> str:
69 | """
70 | Upload the HTML content to a server.
71 |
72 | Args:
73 | file_name: Name of the HTML file to be rendered
74 | html_content: HTML content to be rendered
75 |
76 | Returns:
77 | URL string to access the rendered HTML page
78 | """
79 | try:
80 | response = requests.post(f"{ENDPOINT}/upload_html", json={"file_name": file_name, "file_content": html_content})
81 | response.raise_for_status()
82 | return response.json()['url']
83 | except requests.exceptions.RequestException as e:
84 | raise ValueError(f"Error uploading HTML file: {e}")
85 |
86 |
87 | if __name__ == "__main__":
88 | # print(render_markdown("test.md","## abcd"))
89 | # print(render_html("test2.html","abcd2"))
90 | mcp.run()
91 |
--------------------------------------------------------------------------------
/html_render_service/web/Dockerfile:
--------------------------------------------------------------------------------
1 | # 使用官方的 Python 镜像作为基础镜像
2 | FROM python:3.11-slim
3 |
4 | # 设置工作目录
5 | WORKDIR /app
6 |
7 | # 复制 requirements.txt 并安装依赖
8 | COPY requirements.txt .
9 | RUN pip install -r requirements.txt
10 |
11 | # 复制当前目录下的所有文件到工作目录
12 | COPY . .
13 |
14 | # 设置容器启动时执行的命令
15 | CMD ["python", "main.py"]
--------------------------------------------------------------------------------
/html_render_service/web/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/web/app/__init__.py
--------------------------------------------------------------------------------
/html_render_service/web/app/extensions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/web/app/extensions/__init__.py
--------------------------------------------------------------------------------
/html_render_service/web/app/extensions/checkbox.py:
--------------------------------------------------------------------------------
1 | # stolen from: https://github.com/FND/markdown-checklist/blob/master/markdown_checklist/extension.py
2 | import re
3 |
4 | from markdown.extensions import Extension
5 | from markdown.preprocessors import Preprocessor
6 | from markdown.postprocessors import Postprocessor
7 |
8 |
9 | def makeExtension(configs=None):
10 | if configs is None:
11 | return ChecklistExtension()
12 | else:
13 | return ChecklistExtension(configs=configs)
14 |
15 |
16 | class ChecklistExtension(Extension):
17 |
18 | def __init__(self, **kwargs):
19 | self.config = {
20 | "list_class": ["checklist",
21 | "class name to add to the list element"],
22 | "render_item": [render_item, "custom function to render items"]
23 | }
24 | super().__init__(**kwargs)
25 |
26 | def extendMarkdown(self, md, md_globals):
27 | list_class = self.getConfig("list_class")
28 | renderer = self.getConfig("render_item")
29 | postprocessor = ChecklistPostprocessor(list_class, renderer, md)
30 | md.postprocessors.add("checklist", postprocessor, ">raw_html")
31 |
32 |
33 | class ChecklistPostprocessor(Postprocessor):
34 | """
35 | adds checklist class to list element
36 | """
37 |
38 | list_pattern = re.compile(r"(
\n\[[ Xx]\])")
39 | item_pattern = re.compile(r"^ \[([ Xx])\](.*) $", re.MULTILINE)
40 |
41 | def __init__(self, list_class, render_item, *args, **kwargs):
42 | self.list_class = list_class
43 | self.render_item = render_item
44 | super().__init__(*args, **kwargs)
45 |
46 | def run(self, html):
47 | html = re.sub(self.list_pattern, self._convert_list, html)
48 | return re.sub(self.item_pattern, self._convert_item, html)
49 |
50 | def _convert_list(self, match):
51 | return match.group(1).replace("", f"")
52 |
53 | def _convert_item(self, match):
54 | state, caption = match.groups()
55 | return self.render_item(caption, state != " ")
56 |
57 |
58 | def render_item(caption, checked):
59 | correct = "1" if checked else "0"
60 | fake = "0" if checked else "1"
61 |
62 | return f"" \
63 | f" {caption} " \
64 | f" "
65 |
--------------------------------------------------------------------------------
/html_render_service/web/app/extensions/radio.py:
--------------------------------------------------------------------------------
1 | # stolen from: https://github.com/FND/markdown-checklist/blob/master/markdown_checklist/extension.py
2 | import re
3 |
4 | from markdown.extensions import Extension
5 | from markdown.preprocessors import Preprocessor
6 | from markdown.postprocessors import Postprocessor
7 |
8 |
9 | def makeExtension(configs=None):
10 | if configs is None:
11 | return RadioExtension()
12 | else:
13 | return RadioExtension(configs=configs)
14 |
15 |
16 | class RadioExtension(Extension):
17 |
18 | def __init__(self, **kwargs):
19 | self.config = {
20 | "list_class": ["radio-list", "class name to add to the list element"],
21 | "render_item": [render_item, "custom function to render items"]
22 | }
23 | super().__init__(**kwargs)
24 |
25 | def extendMarkdown(self, md, md_globals):
26 | list_class = self.getConfig("list_class")
27 | renderer = self.getConfig("render_item")
28 | postprocessor = RadioPostprocessor(list_class, renderer, md)
29 | md.postprocessors.add("radio", postprocessor, ">raw_html")
30 |
31 |
32 | class RadioPostprocessor(Postprocessor):
33 | """
34 | adds checklist class to list element
35 | """
36 |
37 | list_pattern = re.compile(r"(\n\([ Xx]\))")
38 | item_pattern = re.compile(r"^ \(([ Xx])\)(.*) $", re.MULTILINE)
39 |
40 | def __init__(self, list_class, render_item, *args, **kwargs):
41 | self.list_class = list_class
42 | self.render_item = render_item
43 | super().__init__(*args, **kwargs)
44 |
45 | def run(self, html):
46 | html = re.sub(self.list_pattern, self._convert_list, html)
47 | return re.sub(self.item_pattern, self._convert_item, html)
48 |
49 | def _convert_list(self, match):
50 | return match.group(1).replace("", f"")
51 |
52 | def _convert_item(self, match):
53 | state, caption = match.groups()
54 | return self.render_item(caption, state != " ")
55 |
56 |
57 | def render_item(caption, checked):
58 | correct = "1" if checked else "0"
59 | fake = "0" if checked else "1"
60 |
61 | return f"" \
62 | f" {caption} " \
63 | f" "
64 |
--------------------------------------------------------------------------------
/html_render_service/web/app/extensions/textbox.py:
--------------------------------------------------------------------------------
1 | # stolen from: https://github.com/FND/markdown-checklist/blob/master/markdown_checklist/extension.py
2 | import re
3 |
4 | from markdown.extensions import Extension
5 | from markdown.preprocessors import Preprocessor
6 | from markdown.postprocessors import Postprocessor
7 |
8 |
9 | def makeExtension(configs=None):
10 | if configs is None:
11 | return TextboxExtension()
12 | else:
13 | return TextboxExtension(configs=configs)
14 |
15 |
16 | class TextboxExtension(Extension):
17 |
18 | def __init__(self, **kwargs):
19 | self.config = {
20 | "list_class": ["textbox", "class name to add to the list element"],
21 | "render_item": [render_item, "custom function to render items"]
22 | }
23 | super().__init__(**kwargs)
24 |
25 | def extendMarkdown(self, md, md_globals):
26 | list_class = self.getConfig("list_class")
27 | renderer = self.getConfig("render_item")
28 | postprocessor = TextboxPostprocessor(list_class, renderer, md)
29 | md.postprocessors.add("textbox", postprocessor, ">raw_html")
30 |
31 |
32 | class TextboxPostprocessor(Postprocessor):
33 | """
34 | adds textbox class to list element
35 | """
36 |
37 | # item_pattern = re.compile(r"^\(([ Xx])\)(.*) $", re.MULTILINE)
38 | list_pattern = re.compile(r"(\n[Rr]:=)")
39 | item_pattern = re.compile(r"^ ([Rr]:=)(.*) $", re.MULTILINE)
40 |
41 | def __init__(self, list_class, render_item, *args, **kwargs):
42 | self.list_class = list_class
43 | self.render_item = render_item
44 | super().__init__(*args, **kwargs)
45 |
46 | def run(self, html):
47 | html = re.sub(self.list_pattern, self._convert_list, html)
48 | return re.sub(self.item_pattern, self._convert_item, html)
49 |
50 | def _convert_list(self, match):
51 | return match.group(1).replace("", f"")
52 |
53 | def _convert_item(self, match):
54 | state, caption = match.groups()
55 | return self.render_item(caption, state != " ")
56 |
57 |
58 | def render_item(caption: str, value):
59 | # the correct answer next to another false one are saved in an attribute such as meta-data written backwards.
60 | correct = caption.strip()[::-1]
61 | fake = "".join([c + 's' for c in correct])
62 | return f"" \
63 | f" "
65 |
--------------------------------------------------------------------------------
/html_render_service/web/app/static/app.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | $('ul.radio-list,ul.checklist,ul.textbox').each(function(i, el){
3 | var questionClass = $(this).attr('class');
4 | $(this).parent().addClass('question-row').addClass(questionClass);
5 | if (questionClass=='radio-list') {
6 | $(this).find('input[type="radio"]').attr('name', 'radio-question-' + i);
7 | }
8 | });
9 |
10 | function checkQuestion() {
11 | resetQuestions(true);
12 | var questions = $('li.question-row');
13 | var total_questions = questions.length;
14 | var correct = 0;
15 |
16 | questions.each(function(i, el) {
17 | var self = $(this);
18 | // Single Question.
19 | if (self.hasClass('radio-list')) {
20 | if (self.find('input[type="radio"][data-content="1"]:checked').length == 1) {
21 | correct += 1;
22 | } else {
23 | self.addClass('text-danger');
24 | }
25 | }
26 | // Textbox Question.
27 | if(self.hasClass('textbox')) {
28 | var textbox = self.find('input[type="text"]');
29 | var correct_text = String(textbox.data("content")).trim().split("").reverse().join("");
30 | if(String(textbox.val()).trim().toLowerCase()==correct_text.toLowerCase()) {
31 | correct += 1;
32 | } else {
33 | self.addClass('text-danger');
34 | textbox.parent().find("i.text-correct").html(correct_text);
35 | }
36 | }
37 | // Multiple selection Questions.
38 | if(self.hasClass('checklist')) {
39 | var total_corrects = self.find('input[type="checkbox"][data-content="1"]').length;
40 | var total_incorrects = self.find('input[type="checkbox"][data-content="0"]').length;
41 | var correct_selected = self.find('input[type="checkbox"][data-content="1"]:checked').length;
42 | var incorrect_selected = self.find('input[type="checkbox"][data-content="0"]:checked').length;
43 | var qc = +((correct_selected / total_corrects) - (incorrect_selected/total_incorrects)).toFixed(2);
44 | if (qc < 0) {
45 | qc = 0;
46 | }
47 | correct += qc;
48 | if (qc == 0) {
49 | self.addClass('text-danger');
50 | } else if (qc > 0 && qc < 1) {
51 | self.addClass('text-warning');
52 | }
53 | }
54 | });
55 |
56 | showScore(correct, total_questions);
57 | }
58 |
59 | function showScore(correct, total) {
60 | var score = (correct / total).toFixed(2) * 100;
61 | var msgClass = 'alert-danger';
62 | if (score >= 70) {
63 | msgClass = 'alert-success';
64 | } else if (score >= 50) {
65 | msgClass = 'alert-warning';
66 | }
67 | $('#tg-correct-questions').text(correct + ' out of ' + total);
68 | $('#tg-score').text(score);
69 | $('#tg-msg').addClass(msgClass).show();
70 | }
71 | function resetQuestions(keep) {
72 | $('li.question-row').removeClass('text-danger').removeClass('text-warning');
73 | $('i.text-correct').html('');
74 | $('#tg-msg').removeClass('alert-danger').removeClass('alert-success').removeClass('alert-warning').hide();
75 | if(keep === true) {
76 | return;
77 | }
78 | $('li.question-row').find('input[type="text"]').val('');
79 | $('li.question-row').find('input[type="radio"],input[type="checkbox"]').prop('checked', false);
80 | }
81 | $('#check-questions').on('click', checkQuestion);
82 | $('#reset-questions').on('click', resetQuestions);
83 |
84 | });
--------------------------------------------------------------------------------
/html_render_service/web/app/static/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | Correct! Rating: %
10 |
11 |
12 | Check
13 | Reset All
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/html_render_service/web/app/static/sample-quiz-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/web/app/static/sample-quiz-animation.gif
--------------------------------------------------------------------------------
/html_render_service/web/app/static/sample-quiz-md-file.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/html_render_service/web/app/static/sample-quiz-md-file.PNG
--------------------------------------------------------------------------------
/html_render_service/web/app/static/wrapper.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
28 |
31 |
32 |
33 | {{ content |safe}}
34 |
35 |
--------------------------------------------------------------------------------
/html_render_service/web/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | flask-web-service:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | image: flask-web-service:latest
7 | container_name: flask-web-service
8 | ports:
9 | - "5006:5006"
10 | volumes:
11 | - ./data:/app/data
12 | - ./files:/app/files
13 | restart: unless-stopped
14 |
--------------------------------------------------------------------------------
/html_render_service/web/main.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request, send_from_directory, jsonify
2 |
3 | import os
4 | import markdown
5 | import time
6 | from jinja2 import Environment, PackageLoader, select_autoescape
7 | import re
8 | import uuid
9 | import requests
10 | # import datatime
11 | app = Flask(__name__)
12 |
13 | # 配置文件夹路径
14 | UPLOAD_FOLDER = './files'
15 | OUTPUT_FOLDER = 'data'
16 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
17 | app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
18 | app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB
19 |
20 | # 确保文件夹存在
21 | os.makedirs(UPLOAD_FOLDER, exist_ok=True)
22 | os.makedirs(OUTPUT_FOLDER, exist_ok=True)
23 |
24 | # def generate_timestamp():
25 | # timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
26 | # return f"- 生成时间:{timestamp}"
27 |
28 | def get_public_ip():
29 | try:
30 | # 使用外部服务获取公网 IP
31 | response = requests.get('https://api.ipify.org').text
32 | return response.strip()
33 | except:
34 | return None
35 |
36 | host_ip = get_public_ip()
37 |
38 |
39 | @app.route('/upload_html',methods=['POST'])
40 | def upload_html():
41 | # host_ip = get_public_ip()
42 | file_suffix = str(uuid.uuid4())[:8]
43 |
44 | # Check if the request contains JSON with file_name and file_content
45 | if request.is_json:
46 | json_data = request.get_json()
47 | if 'file_name' in json_data and 'file_content' in json_data:
48 | file_name = json_data['file_name']
49 | content = json_data['file_content']
50 |
51 | # Remove .html extension if present
52 | if file_name.endswith('.html'):
53 | filename = file_name[:-5]
54 | else:
55 | filename = file_name
56 | else:
57 | return jsonify({"error": "JSON must contain file_name and file_content fields"}), 400
58 | # Check if the request contains a file upload
59 | elif 'file' in request.files:
60 | file = request.files['file']
61 |
62 | # Get the original filename
63 | original_filename = file.filename
64 |
65 | # Remove .html extension if present
66 | if original_filename.endswith('.html'):
67 | filename = original_filename[:-5]
68 | else:
69 | filename = original_filename
70 |
71 | # Get content from file
72 | content = file.read().decode('utf-8')
73 | # Handle direct content upload as fallback
74 | else:
75 | # Direct content upload
76 | content = request.get_data(as_text=True)
77 | if content is None:
78 | return jsonify({"error": "Invalid input"}), 400
79 |
80 | # Use a timestamp as filename since no original filename is available
81 | filename = f"html_content"
82 |
83 | filename = f"{filename}_{file_suffix}"
84 |
85 | # Save the HTML content directly without modifications
86 | with open(os.path.join(OUTPUT_FOLDER, f"{filename}.html"), "w+", encoding='utf-8') as f:
87 | f.write(content)
88 |
89 | return jsonify(
90 | {"url":
91 | f"http://{host_ip}:5006/get_html/{filename}.html"}), 200
92 |
93 | @app.route('/upload_markdown', methods=['POST'])
94 | def upload_markdown():
95 |
96 | file_suffix = str(uuid.uuid4())[:8]
97 | # Check if the request contains JSON with file_name and file_content
98 | if request.is_json:
99 | json_data = request.get_json()
100 | if 'file_name' in json_data and 'file_content' in json_data:
101 | file_name = json_data['file_name']
102 | content = json_data['file_content']
103 |
104 | # Remove .md extension if present
105 | if file_name.endswith('.md'):
106 | filename = file_name[:-3]
107 | else:
108 | filename = file_name
109 | else:
110 | return jsonify({"error": "JSON must contain file_name and file_content fields"}), 400
111 | # Check if the request contains a file upload
112 | elif 'file' in request.files:
113 | file = request.files['file']
114 |
115 | # Get the original filename
116 | original_filename = file.filename
117 |
118 | # Remove .md extension if present
119 | if original_filename.endswith('.md'):
120 | filename = original_filename[:-3]
121 | else:
122 | filename = original_filename
123 |
124 | # Get content from file
125 | content = file.read().decode('utf-8')
126 | # Handle direct content upload as fallback
127 | else:
128 | content = request.get_data(as_text=True)
129 | if content is None:
130 | return jsonify({"error": "Invalid input"}), 400
131 |
132 | # Use UUID for filename when no name is provided
133 | filename = str(uuid.uuid4())
134 |
135 |
136 | html_content = markdown.markdown(content)
137 | complete_html = f"""
138 |
139 |
140 |
141 | Markdown Rendered
142 |
143 |
144 | {html_content}
145 |
146 |
147 | """
148 | filename = f"{filename}_{file_suffix}"
149 | with open(os.path.join(OUTPUT_FOLDER, f"{filename}.html"),
150 | "w+",
151 | encoding='utf-8') as f: # create final file
152 | f.write(complete_html)
153 |
154 | return jsonify(
155 | {"url":
156 | f"http://{host_ip}:5006/get_html/{filename}.html"}), 200
157 |
158 | @app.route('/',methods=['GET'])
159 | def ping():
160 | return jsonify({"message": "ok"}), 200
161 |
162 | @app.route('/get_html/', methods=['GET'])
163 | def get_html(filename):
164 | if not filename.endswith('.html'):
165 | filename += '.html'
166 |
167 | file_path = os.path.join(app.config['OUTPUT_FOLDER'], filename)
168 | if not os.path.exists(file_path):
169 | return jsonify({"error": "File not found"}), 404
170 |
171 | return send_from_directory(app.config['OUTPUT_FOLDER'], filename)
172 |
173 |
174 | if __name__ == '__main__':
175 | app.run(debug=True, port=5006, host='0.0.0.0')
176 |
--------------------------------------------------------------------------------
/html_render_service/web/requirements.txt:
--------------------------------------------------------------------------------
1 | Jinja2==3.1.2
2 | Markdown==3.1.1
3 | MarkupSafe==2.1.1
4 | flask==2.3.3
5 | requests==2.32.3
--------------------------------------------------------------------------------
/remote_computer_use/.env.example:
--------------------------------------------------------------------------------
1 | # VNC Connection Settings
2 | VNC_HOST=192.168.1.100
3 | VNC_PORT=5900
4 | VNC_USERNAME=ubuntu
5 | VNC_PASSWORD=your_vnc_password
6 | SSH_PORT=22
7 | PEM_FILE=your_pem_file.pem
8 | DISPLAY_NUM=1
9 |
--------------------------------------------------------------------------------
/remote_computer_use/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 | .env
9 | # Virtual environments
10 | .venv
11 | .DS_Store
--------------------------------------------------------------------------------
/remote_computer_use/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/remote_computer_use/INSTALL.md:
--------------------------------------------------------------------------------
1 | # Computer Use MCP Server - Installation Guide
2 |
3 | This guide provides step-by-step instructions for installing and configuring the Computer Use MCP Server.
4 |
5 | ## Prerequisites
6 |
7 | Before you begin, ensure you have the following:
8 | 1. **Remote Desktop on Ubuntu 24.04** You can use an AWS EC2 instance with Ubuntu 24.04
9 | 2. **VNC Server** running on your remote Ubuntu machine
10 | 3. **xdotool** installed on your remote Ubuntu machine
11 | 4. **SSH access** to your remote Ubuntu machine
12 |
13 | ## Installation Steps
14 |
15 | ## Install VNC remote desktop on ubuntu 24.04
16 | 1. First, update your system packages:
17 | ```bash
18 | sudo apt update
19 | sudo apt upgrade
20 | ```
21 | - install necessary apps
22 | ```bash
23 | sudo apt install -y xdotool
24 | sudo apt install -y scrot
25 | sudo add-apt-repository ppa:mozillateam/ppa && \
26 | sudo apt-get install -y --no-install-recommends \
27 | libreoffice \
28 | firefox-esr
29 | ```
30 |
31 | 2. Install a Desktop Environment
32 | ```bash
33 | sudo apt install xfce4 xfce4-goodies
34 | ```
35 |
36 | 3. Install TigerVNC Server
37 | ```bash
38 | sudo apt install tigervnc-standalone-server tigervnc-common
39 | ```
40 |
41 | 4. Set up a password for VNC access:
42 | ```bash
43 | vncpasswd
44 | ```
45 |
46 | 5. Create or edit the VNC startup file:
47 | ```bash
48 | vim ~/.vnc/xstartup
49 | ```
50 | - For Xfce, add the following content:
51 | ```bash
52 | #!/bin/sh
53 |
54 | unset SESSION_MANAGER
55 | unset DBUS_SESSION_BUS_ADDRESS
56 |
57 | [ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup
58 | [ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
59 |
60 | export XKL_XMODMAP_DISABLE=1
61 | startxfce4
62 | ```
63 |
64 | 6. Make the startup file executable:
65 | ```bash
66 | chmod +x ~/.vnc/xstartup
67 | ```
68 |
69 | 7. Starting the VNC Server
70 | ```bash
71 | tigervncserver -xstartup /usr/bin/startxfce4 -SecurityTypes VncAuth,TLSVnc -geometry 1024x768 -localhost no :1
72 | ```
73 |
74 | 8. Open (TCP : 5901) in your Securty Group of EC2
75 |
76 | 9. Optional: stop the VNC server
77 | ```bash
78 | vncserver -kill :*
79 | ```
80 |
81 | ### 4. Test Your Connection
82 | Edit the `.env` file created by the setup script:
83 |
84 | ```bash
85 | # VNC Connection Settings
86 | VNC_HOST=192.168.1.100 # Replace with your Ubuntu server IP
87 | VNC_PORT=5900 # Default VNC port
88 | VNC_USERNAME=ubuntu # Your username
89 | VNC_PASSWORD=password # Your VNC password
90 | PEM_FILE=your_pem_file.pem
91 | DISPLAY_NUM=1
92 | # SSH Connection Settings (uses same host as VNC)
93 | SSH_PORT=22 # Default SSH port
94 | ```
95 | Before running the MCP server, test your VNC and SSH connections:
96 |
97 | ```bash
98 | # Activate the virtual environment
99 | source venv/bin/activate
100 |
101 | # Run the connection test
102 | ./test_connection.py
103 | ```
104 |
105 | This will verify that:
106 | - Your VNC connection works and can capture screenshots
107 | - Your SSH connection works
108 |
109 | If any tests fail, check the error messages and troubleshoot accordingly.
110 |
111 |
112 | This will start the MCP Inspector interface where you can test the server's tools.
113 |
114 |
115 | ### Add below json to your client to install MCP Server,
116 | this `server_claude.py` works better for Claude 3.5/3.7, if you want to use other models, please change the run command to run `server.py` instead.
117 | ```json
118 | {
119 | "mcpServers": {
120 | "computer_use": {
121 | "command": "uv",
122 | "env": {
123 | "VNC_HOST":"",
124 | "VNC_PORT":"5901",
125 | "VNC_USERNAME":"ubuntu",
126 | "VNC_PASSWORD":"",
127 | "PEM_FILE":"",
128 | "WIDTH":"1024",
129 | "HEIGHT":"768",
130 | "SSH_PORT":"22",
131 | "DISPLAY_NUM":"1"
132 | },
133 | "args": [
134 | "--directory",
135 | "/absolute_path_to/remote_computer_use",
136 | "run",
137 | "server_claude.py"
138 | ]
139 | }
140 | }
141 | }
142 | ```
--------------------------------------------------------------------------------
/remote_computer_use/README.md:
--------------------------------------------------------------------------------
1 | # Computer Use MCP Server
2 |
3 | A Model Context Protocol (MCP) server that enables remote control of an Ubuntu desktop use [Computer Use](https://docs.anthropic.com/en/docs/agents-and-tools/computer-use)
4 |
5 | ## Features
6 | Support `computer tool` and `bash tool`, and `text_editor tool` defined by Claude 3.5/3.7
7 | - **Mouse Control**: Move, click, and scroll
8 | - **Keyboard Control**: Type text and press keys
9 | - **Screenshot Capture**: Get visual feedback after each operation
10 | - **bash command** : Run bash command via ssh
11 | - **edit tool** : str_replace_editor
12 |
13 | ## Prerequisites
14 | - VNC server running on the remote Ubuntu machine
15 | - xdotool installed on the remote Ubuntu machine
16 | - SSH access to the remote Ubuntu machine
17 |
18 | ## Installation
19 | ### (New) Recommend Option 1
20 | 1. Build and run a ubuntu 24.04 sandbox in docker container
21 | [INSTALL Docker](docker/README.md)
22 |
23 | ### Option 2
24 | 1. Install Remote Desktop in standalone EC2 instance
25 | [INSTALL](./INSTALL.md)
26 |
27 | ## Add system prompt to your client when use computer_use
28 | ```
29 | You are a computer agent, you can actually operate a vitural computer.
30 | you have capability:
31 |
32 | * You are utilising an Ubuntu virtual machine using Linux architecture with internet access.
33 | * You can feel free to install Ubuntu applications with your bash tool. Use curl instead of wget.
34 | * When viewing a page it can be helpful to zoom out so that you can see everything on the page. Either that, or make sure you scroll down to see everything before deciding something isn't available.
35 | * When using your computer function calls, they take a while to run and send back to you. Where possible/feasible, try to chain multiple of these calls all into one function calls request.
36 |
37 |
38 |
39 | * Don't assume an application's coordinates are on the screen unless you saw the screenshot. To open an application, please take screenshot first and then find out the coordinates of the application icon.
40 | * When using Firefox, if a startup wizard or Firefox Privacy Notice appears, IGNORE IT. Do not even click "skip this step". Instead, click on the address bar where it says "Search or enter address", and enter the appropriate search term or URL there.
41 | * If the item you are looking at is a pdf, if after taking a single screenshot of the pdf it seems that you want to read the entire document instead of trying to continue to read the pdf from your screenshots + navigation, determine the URL, use curl to download the pdf, install and use pdftotext to convert it to a text file, and then read that text file directly with your StrReplaceEditTool.
42 | * After each step, take a screenshot and carefully evaluate if you have achieved the right outcome. Explicitly show your thinking: "I have evaluated step X..." If not correct, try again. Only when you confirm a step was executed correctly should you move on to the next one.
43 |
44 | ```
45 |
46 | ## examples
47 | 
48 |
49 |
50 |
--------------------------------------------------------------------------------
/remote_computer_use/assets/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-mcp-servers-samples/69c58ab6ee4ca135ebb71ba90f8753e1224497d5/remote_computer_use/assets/image1.png
--------------------------------------------------------------------------------
/remote_computer_use/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 |
3 | # 避免交互式提示
4 | ENV DEBIAN_FRONTEND=noninteractive
5 |
6 | # 安装必要的软件包
7 | RUN apt-get update && \
8 | apt-get upgrade -y && \
9 | apt-get install -y sudo apt-utils software-properties-common && \
10 | apt-get install -y xdotool scrot && \
11 | # 添加Mozilla PPA
12 | add-apt-repository ppa:mozillateam/ppa -y || true && \
13 | apt-get update && \
14 | # 安装所需应用
15 | apt-get install -y --no-install-recommends firefox-esr galculator libreoffice xpdf && \
16 | # 安装Xfce桌面环境
17 | apt-get install -y xfce4 xfce4-goodies dbus-x11 && \
18 | # 安装TigerVNC和SSH服务器
19 | apt-get install -y tigervnc-standalone-server tigervnc-common openssh-server openssh-client && \
20 | apt-get clean && \
21 | rm -rf /var/lib/apt/lists/* && \
22 | mkdir -p /var/run/sshd
23 |
24 | # 创建非root用户
25 | RUN useradd -m -s /bin/bash vnc_user && \
26 | echo "vnc_user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
27 |
28 | # 配置SSH服务器
29 | RUN echo "PermitRootLogin no" >> /etc/ssh/sshd_config && \
30 | echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config && \
31 | echo "X11Forwarding yes" >> /etc/ssh/sshd_config
32 |
33 | # 切换到非root用户
34 | USER vnc_user
35 | WORKDIR /home/vnc_user
36 |
37 | # # 创建用于显示应用的桌面目录
38 | # RUN mkdir -p /home/vnc_user/Desktop
39 |
40 | # # 创建桌面快捷方式
41 | # RUN echo "[Desktop Entry]\n\
42 | # Type=Application\n\
43 | # Name=Firefox ESR\n\
44 | # Comment=Web Browser\n\
45 | # Exec=firefox-esr %u\n\
46 | # Icon=firefox-esr\n\
47 | # Terminal=false\n\
48 | # Categories=Network;WebBrowser;\n\
49 | # StartupNotify=true\n\
50 | # X-GNOME-Autostart-enabled=true\n\
51 | # X-XFCE-Source=file:/home/vnc_user/Desktop/firefox.desktop" > /home/vnc_user/Desktop/firefox.desktop && \
52 | # \
53 | # echo "[Desktop Entry]\n\
54 | # Type=Application\n\
55 | # Name=Galculator\n\
56 | # Comment=Scientific Calculator\n\
57 | # Exec=galculator\n\
58 | # Icon=galculator\n\
59 | # Terminal=false\n\
60 | # Categories=Utility;Calculator;\n\
61 | # StartupNotify=true" > /home/vnc_user/Desktop/galculator.desktop && \
62 | # \
63 | # echo "[Desktop Entry]\n\
64 | # Type=Application\n\
65 | # Name=LibreOffice\n\
66 | # Comment=Office Suite\n\
67 | # Exec=libreoffice %U\n\
68 | # Icon=libreoffice-startcenter\n\
69 | # Terminal=false\n\
70 | # Categories=Office;\n\
71 | # StartupNotify=true" > /home/vnc_user/Desktop/libreoffice.desktop && \
72 | # \
73 | # echo "[Desktop Entry]\n\
74 | # Type=Application\n\
75 | # Name=Xpdf\n\
76 | # Comment=PDF Viewer\n\
77 | # Exec=xpdf %f\n\
78 | # Icon=xpdf\n\
79 | # Terminal=false\n\
80 | # Categories=Viewer;PDF;\n\
81 | # StartupNotify=true" > /home/vnc_user/Desktop/xpdf.desktop && \
82 | # \
83 | # chmod +x /home/vnc_user/Desktop/*.desktop
84 |
85 | # 创建VNC和SSH配置目录
86 | RUN mkdir -p ~/.vnc ~/.ssh
87 |
88 | # 创建运行时目录
89 | RUN mkdir -p /tmp/runtime-vnc_user
90 |
91 | # 复制VNC配置文件
92 | COPY --chown=vnc_user:vnc_user xstartup /home/vnc_user/.vnc/xstartup
93 | COPY --chown=vnc_user:vnc_user start_vnc.sh /home/vnc_user/start_vnc.sh
94 |
95 | # 设置执行权限
96 | RUN chmod +x ~/.vnc/xstartup && \
97 | chmod +x ~/start_vnc.sh
98 |
99 | # 生成SSH密钥
100 | RUN ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
101 |
102 | # 暴露VNC和SSH端口
103 | EXPOSE 5901 22
104 |
105 | # 使用root用户启动服务
106 | USER root
107 | CMD ["/home/vnc_user/start_vnc.sh"]
108 |
--------------------------------------------------------------------------------
/remote_computer_use/docker/README.md:
--------------------------------------------------------------------------------
1 | ## Build and run a ubuntu 24.04 sandbox in docker container
2 |
3 | ### Prequisite
4 | 1. Install docker-compose if you did not install it.
5 | ```bash
6 | sudo curl -SL https://github.com/docker/compose/releases/download/v2.35.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
7 | sudo chmod +x /usr/local/bin/docker-compose
8 | sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
9 | ```
10 |
11 | 2. Start container
12 | ```bash
13 | docker-compose up -d
14 | ```
15 |
16 | ### Add below json to your client to install MCP Server,
17 | - Change the path to the remote_computer_use/server_claude.py.
18 | - If you run the VNC container in a seperate EC2, please change VNC_HOST to the actual ip address.
19 | ```json
20 | {
21 | "mcpServers": {
22 | "computer_use_docker": {
23 | "command": "uv",
24 | "env": {
25 | "VNC_HOST":"127.0.0.1",
26 | "VNC_PORT":"5901",
27 | "VNC_USERNAME":"vnc_user",
28 | "VNC_PASSWORD":"12345670",
29 | "PEM_FILE":"",
30 | "SSH_PORT":"2222",
31 | "DISPLAY_NUM":"1",
32 | "WIDTH":"1024",
33 | "HEIGHT":"768"
34 | },
35 | "args": [
36 | "--directory",
37 | "/absolute_path/to/remote_computer_use",
38 | "run",
39 | "server_claude.py"
40 | ]
41 | }
42 | }
43 | }
44 | ```
45 |
46 | ### Other commands
47 | 1. Stop container
48 | ```bash
49 | docker-compose down
50 | ```
51 | 2. View logs of container
52 | ```bash
53 | docker-compose logs -f
54 | ```
--------------------------------------------------------------------------------
/remote_computer_use/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ubuntu-vnc:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | image: ubuntu-vnc:24.04
7 | container_name: ubuntu-vnc-desktop
8 | ports:
9 | - "5901:5901"
10 | - "2222:22" # 将容器的22端口映射到主机的2222端口
11 | restart: unless-stopped
12 | volumes:
13 | - ./data:/home/vnc_user/data
14 | environment:
15 | - VNC_RESOLUTION=1024x768
16 | - VNC_PASSWORD=12345670 # 自定义VNC密码
17 | - SSH_PASSWORD=12345670 # 自定义SSH密码
18 |
--------------------------------------------------------------------------------
/remote_computer_use/docker/start_vnc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 设置SSH密码
4 | echo "vnc_user:${SSH_PASSWORD:-vncpassword}" | chpasswd
5 |
6 | # 启动SSH服务
7 | /usr/sbin/sshd
8 |
9 | # 设置环境变量
10 | export XDG_RUNTIME_DIR=/tmp/runtime-vnc_user
11 | mkdir -p $XDG_RUNTIME_DIR
12 | chmod 700 $XDG_RUNTIME_DIR
13 |
14 | # 切换到vnc_user用户运行VNC
15 | su - vnc_user -c "
16 | # 设置VNC密码
17 | mkdir -p ~/.vnc
18 | echo \"${VNC_PASSWORD:-vncpassword}\" | vncpasswd -f > ~/.vnc/passwd
19 | chmod 600 ~/.vnc/passwd
20 |
21 | # 清理所有旧的VNC服务器进程
22 | vncserver -kill :1 >/dev/null 2>&1 || :
23 | rm -rf /tmp/.X1-lock /tmp/.X11-unix/X1 >/dev/null 2>&1 || :
24 |
25 | # 启动VNC服务器
26 | vncserver :1 -geometry \"${VNC_RESOLUTION}\" -depth 24 -localhost no
27 |
28 | echo \"VNC Server started on port 5901\"
29 | echo \"Use a VNC viewer to connect to \$(hostname -I | awk '{print \$1}'):5901\"
30 | echo \"SSH access available on port 22, username: vnc_user\"
31 | "
32 |
33 | # 保持容器运行
34 | tail -f /home/vnc_user/.vnc/*:1.log
35 |
--------------------------------------------------------------------------------
/remote_computer_use/docker/xstartup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # 设置环境变量
4 | export XDG_RUNTIME_DIR=/tmp/runtime-vnc_user
5 |
6 | unset SESSION_MANAGER
7 | unset DBUS_SESSION_BUS_ADDRESS
8 |
9 | # 启动dbus
10 | /etc/init.d/dbus start
11 |
12 | # 初始化Xfce会话
13 | exec startxfce4
14 |
--------------------------------------------------------------------------------
/remote_computer_use/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "remote-computer-use"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.11"
7 | dependencies = [
8 | "anthropic>=0.49.0",
9 | "httpx>=0.28.1",
10 | "mcp[cli]>=1.4.1",
11 | "paramiko>=3.5.1",
12 | "pillow>=11.1.0",
13 | "python-dotenv>=1.0.1",
14 | "shortuuid>=1.0.13",
15 | "vncdotool>=1.2.0",
16 | ]
17 |
--------------------------------------------------------------------------------
/remote_computer_use/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Computer Use MCP Server
4 | Provides tools for controlling a remote Ubuntu desktop via VNC and xdotool
5 | """
6 | import os
7 | import asyncio
8 | from dataclasses import dataclass
9 | from contextlib import asynccontextmanager
10 | from typing import AsyncIterator, Optional, Dict, Any, List
11 | import io
12 | from mcp.server.fastmcp import FastMCP, Image, Context
13 | from vnc_controller import VNCController
14 | from ssh_controller import SSHController
15 | import time
16 | # import dotenv
17 | # dotenv.load_dotenv()
18 |
19 | # Create dataclass for app context
20 | @dataclass
21 | class AppContext:
22 | """Application context for lifespan management"""
23 | vnc: VNCController
24 | ssh: SSHController
25 | display_num : str
26 |
27 | # Define lifespan for connection management
28 | @asynccontextmanager
29 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
30 | """
31 | Manage VNC and SSH connections lifecycle
32 |
33 | Args:
34 | server: FastMCP server instance
35 |
36 | Yields:
37 | AppContext: Application context with VNC and SSH controllers
38 | """
39 | # Get credentials from environment variables
40 | vnc_host = os.environ.get("VNC_HOST")
41 | vnc_port = int(os.environ.get("VNC_PORT", "5900"))
42 | vnc_username = os.environ.get("VNC_USERNAME")
43 | vnc_password = os.environ.get("VNC_PASSWORD")
44 | pem_file = os.environ.get("PEM_FILE", "")
45 | ssh_port = int(os.environ.get("SSH_PORT", "22"))
46 | display_num = os.environ.get("DISPLAY_NUM", "1")
47 |
48 |
49 | # Validate required environment variables
50 | if not vnc_host:
51 | raise ValueError("VNC_HOST environment variable is required")
52 | if not vnc_password:
53 | raise ValueError("VNC_PASSWORD environment variable is required")
54 | if not vnc_username:
55 | raise ValueError("VNC_USERNAME environment variable is required")
56 |
57 | # Initialize controllers
58 | vnc_controller = VNCController(vnc_host, vnc_port, vnc_username, vnc_password)
59 | ssh_controller = SSHController(vnc_host, ssh_port, vnc_username, vnc_password,pem_file, display_num)
60 |
61 | try:
62 | # Connect on startup
63 | vnc_success = await vnc_controller.connect()
64 | if not vnc_success:
65 | print("Warning: Failed to connect to VNC server on startup")
66 |
67 | ssh_success = await ssh_controller.connect()
68 | if not ssh_success:
69 | print("Warning: Failed to connect to SSH server on startup")
70 |
71 | # Yield context to server
72 | yield AppContext(vnc=vnc_controller, ssh=ssh_controller,display_num=display_num)
73 | finally:
74 | # Disconnect on shutdown
75 | await vnc_controller.disconnect()
76 | await ssh_controller.disconnect()
77 |
78 | # Create MCP server
79 | mcp = FastMCP(
80 | "Computer Use",
81 | dependencies=["pillow", "paramiko", "vncdotool"],
82 | lifespan=app_lifespan
83 | )
84 |
85 |
86 | # Define MCP tools
87 | @mcp.tool()
88 | async def capture_region(ctx: Context,x: int, y: int, w: int, h: int) -> Image:
89 | """
90 | Capture screenshot only represents of a region of the remote desktop
91 |
92 | Args:
93 | x: X coordinate (pixels from the left edge)
94 | y: Y coordinate (pixels from the top edge)
95 | w: Width of the region
96 | h: Hight of the region
97 |
98 | Returns:
99 | Image: Screenshot of the remote desktop
100 | """
101 | vnc = ctx.request_context.lifespan_context.vnc
102 | screenshot = await vnc.capture_region(x,y,w,h)
103 |
104 | # Convert PIL Image to bytes
105 | img_bytes = io.BytesIO()
106 | screenshot.save(img_bytes, format="PNG")
107 |
108 | return Image(data=img_bytes.getvalue(), format="png")
109 |
110 | @mcp.tool()
111 | async def capture_screenshot(ctx: Context) -> Image:
112 | """
113 | Capture a screenshot of the remote desktop
114 |
115 | Returns:
116 | Image: Screenshot of the remote desktop
117 | """
118 | vnc = ctx.request_context.lifespan_context.vnc
119 | try:
120 | screenshot = await vnc.capture_screenshot()
121 | except Exception as e:
122 | raise ValueError(f"{e}")
123 |
124 |
125 | # Convert PIL Image to bytes
126 | img_bytes = io.BytesIO()
127 | screenshot.save(img_bytes, format="PNG")
128 |
129 | return Image(data=img_bytes.getvalue(), format="png")
130 |
131 | @mcp.tool()
132 | async def mouse_double_click(ctx: Context, x: int, y: int) -> Image:
133 | """
134 | Double-click the left mouse button at the specified coordinates
135 |
136 | Args:
137 | x: X coordinate (pixels from the left edge)
138 | y: Y coordinate (pixels from the top edge)
139 |
140 | Returns:
141 | str: execution result
142 | """
143 | vnc = ctx.request_context.lifespan_context.vnc
144 | try:
145 | await vnc.mouse_click(x, y, 1)
146 | time.sleep(0.1)
147 | await vnc.mouse_click(x, y, 1)
148 | time.sleep(3)
149 | except:
150 | raise ValueError(f"Failed to double-click, error:{e}")
151 |
152 | return 'Double-click executed, please capture a new screenshot in next turn to see the result'
153 |
154 | @mcp.tool()
155 | async def mouse_click(ctx: Context, x: int, y: int, button: int = 1) -> Image:
156 | """
157 | Click at the specified coordinates and return a screenshot,
158 |
159 | Args:
160 | x: X coordinate (pixels from the left edge)
161 | y: Y coordinate (pixels from the top edge)
162 | button: Mouse button (1=Click the left mouse button, 2=Click the middle mouse button, 3=Click the right mouse button)
163 |
164 | Returns:
165 | Image: Screenshot after clicking
166 | """
167 | vnc = ctx.request_context.lifespan_context.vnc
168 | try:
169 | await vnc.mouse_click(x, y, button)
170 |
171 | # Capture screenshot after clicking
172 | screenshot = await vnc.capture_screenshot()
173 | except Exception as e:
174 | raise ValueError(f"{e}")
175 |
176 | # Convert PIL Image to bytes
177 | img_bytes = io.BytesIO()
178 | screenshot.save(img_bytes, format="PNG")
179 |
180 | return Image(data=img_bytes.getvalue(), format="png")
181 |
182 | @mcp.tool()
183 | async def mouse_move(ctx: Context, x: int, y: int) -> Image:
184 | """
185 | Move mouse to the specified coordinates and return a screenshot
186 |
187 | Args:
188 | x: X coordinate (pixels from the left edge)
189 | y: Y coordinate (pixels from the top edge)
190 |
191 | Returns:
192 | Image: Screenshot after moving mouse
193 | """
194 | vnc = ctx.request_context.lifespan_context.vnc
195 | try:
196 | await vnc.mouse_move(x, y)
197 | # Capture screenshot after moving mouse
198 | screenshot = await vnc.capture_screenshot()
199 | except Exception as e:
200 | raise ValueError(f"{e}")
201 |
202 | # Convert PIL Image to bytes
203 | img_bytes = io.BytesIO()
204 | screenshot.save(img_bytes, format="PNG")
205 |
206 | return Image(data=img_bytes.getvalue(), format="png")
207 |
208 | @mcp.tool()
209 | async def mouse_scroll(ctx: Context, steps: int = 1, direction: str = "down") -> Image:
210 | """
211 | Scroll the mouse wheel and return a screenshot
212 |
213 | Args:
214 | steps: Number of scroll steps
215 | direction: 'up' or 'down'
216 |
217 | Returns:
218 | Image: Screenshot after scrolling
219 | """
220 | vnc = ctx.request_context.lifespan_context.vnc
221 |
222 | try:
223 | await vnc.mouse_scroll(steps, direction)
224 |
225 | # Capture screenshot after scrolling
226 | screenshot = await vnc.capture_screenshot()
227 | except Exception as e:
228 | raise ValueError(f"{e}")
229 |
230 | # Convert PIL Image to bytes
231 | img_bytes = io.BytesIO()
232 | screenshot.save(img_bytes, format="PNG")
233 |
234 | return Image(data=img_bytes.getvalue(), format="png")
235 |
236 | @mcp.tool()
237 | async def type_text(ctx: Context, text: str) -> Image:
238 | """
239 | Type the specified text and return a screenshot
240 |
241 | Args:
242 | text: Text to type
243 |
244 | Returns:
245 | Image: Screenshot after typing text
246 | """
247 | vnc = ctx.request_context.lifespan_context.vnc
248 | try :
249 | await vnc.type_text(text)
250 |
251 | # Capture screenshot after typing
252 | screenshot = await vnc.capture_screenshot()
253 | except Exception as e:
254 | raise ValueError(f"{e}")
255 |
256 | # Convert PIL Image to bytes
257 | img_bytes = io.BytesIO()
258 | screenshot.save(img_bytes, format="PNG")
259 |
260 | return Image(data=img_bytes.getvalue(), format="png")
261 |
262 | @mcp.tool()
263 | async def key_press(ctx: Context, key: str) -> Image:
264 | """
265 | Press a key and return a screenshot
266 |
267 | Args:
268 | key: Key to press (e.g., 'enter', 'escape', etc.)
269 |
270 | Returns:
271 | Image: Screenshot after pressing key
272 | """
273 | vnc = ctx.request_context.lifespan_context.vnc
274 | try:
275 | await vnc.key_press(key)
276 |
277 | # Capture screenshot after pressing key
278 | screenshot = await vnc.capture_screenshot()
279 | except Exception as e:
280 | raise ValueError(f"{e}")
281 | # Convert PIL Image to bytes
282 | img_bytes = io.BytesIO()
283 | screenshot.save(img_bytes, format="PNG")
284 |
285 | return Image(data=img_bytes.getvalue(), format="png")
286 |
287 | @mcp.tool()
288 | async def execute_bash(ctx: Context, command: str,restart: bool= False) -> Dict[str, Any]:
289 | """
290 | Run commands in a bash shell
291 | * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
292 | * You have access to a mirror of common linux and python packages via apt and pip.
293 | * State is persistent across command calls and discussions with the user.
294 | * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
295 | * Please avoid commands that may produce a very large amount of output.
296 | * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
297 | Args:
298 | command: The bash command to run. Required unless the tool is being restarted.
299 | restart: Specifying true will restart this tool. Otherwise, leave this unspecified. Defaut to False
300 | Returns:
301 | dict: Command execution result
302 | """
303 | ssh = ctx.request_context.lifespan_context.ssh
304 | if restart:
305 | ssh_success = await ssh.connect()
306 | if not ssh_success:
307 | return "Failed to connect to SSH server on startup"
308 |
309 | result = await ssh.execute_command(f"{command}")
310 | return result
311 |
312 | # Run server if executed directly
313 | if __name__ == "__main__":
314 | mcp.run()
--------------------------------------------------------------------------------
/remote_computer_use/server_claude.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Computer Use MCP Server
4 | Provides tools for controlling a remote Ubuntu desktop via VNC and xdotool
5 | """
6 | import os
7 | import asyncio
8 | from dataclasses import dataclass
9 | from contextlib import asynccontextmanager
10 | from typing import AsyncIterator, Optional, Dict, Any, List
11 | import io
12 | import json
13 | from mcp.server.fastmcp import FastMCP, Image, Context
14 | from vnc_controller import VNCController
15 | from ssh_controller import SSHController
16 | from tools.computer import ComputerTool20250124 as ComputerTool
17 | from tools.bash import BashTool
18 | from tools.edit import Command,EditTool
19 | import time
20 | import base64
21 | # from PIL import Image
22 | from tools.computer import Action,Action_20250124,ScrollDirection
23 | import functools
24 | # import dotenv
25 | # dotenv.load_dotenv()
26 |
27 |
28 |
29 |
30 | # Create dataclass for app context
31 | @dataclass
32 | class AppContext:
33 | """Application context for lifespan management"""
34 | vnc: VNCController
35 | ssh: SSHController
36 | display_num : str
37 |
38 | # Define lifespan for connection management
39 | @asynccontextmanager
40 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
41 | """
42 | Manage VNC and SSH connections lifecycle
43 |
44 | Args:
45 | server: FastMCP server instance
46 |
47 | Yields:
48 | AppContext: Application context with VNC and SSH controllers
49 | """
50 | # Get credentials from environment variables
51 | vnc_host = os.environ.get("VNC_HOST")
52 | vnc_port = int(os.environ.get("VNC_PORT", "5900"))
53 | vnc_username = os.environ.get("VNC_USERNAME")
54 | vnc_password = os.environ.get("VNC_PASSWORD")
55 | pem_file = os.environ.get("PEM_FILE", "")
56 | ssh_port = int(os.environ.get("SSH_PORT", "22"))
57 | display_num = os.environ.get("DISPLAY_NUM", "1")
58 |
59 | # Validate required environment variables
60 | if not vnc_host:
61 | raise ValueError("VNC_HOST environment variable is required")
62 | if not vnc_password:
63 | raise ValueError("VNC_PASSWORD environment variable is required")
64 | if not vnc_username:
65 | raise ValueError("VNC_USERNAME environment variable is required")
66 |
67 | # Initialize controllers
68 | vnc_controller = VNCController(vnc_host, vnc_port, vnc_username, vnc_password)
69 | ssh_controller = SSHController(vnc_host, ssh_port, vnc_username, vnc_password,pem_file, display_num)
70 |
71 | try:
72 | # Connect on startup
73 | vnc_success = await vnc_controller.connect()
74 | if not vnc_success:
75 | print("Warning: Failed to connect to VNC server on startup")
76 |
77 | ssh_success = await ssh_controller.connect()
78 | if not ssh_success:
79 | print("Warning: Failed to connect to SSH server on startup")
80 |
81 | # Yield context to server
82 | yield AppContext(vnc=vnc_controller, ssh=ssh_controller,display_num=display_num)
83 | finally:
84 | # Disconnect on shutdown
85 | await vnc_controller.disconnect()
86 | await ssh_controller.disconnect()
87 |
88 | # Create MCP server
89 | mcp = FastMCP(
90 | "Computer Use",
91 | dependencies=["pillow", "paramiko", "vncdotool","python-dotenv"],
92 | lifespan=app_lifespan
93 | )
94 |
95 |
96 | def update_docstring_with_display_info(func):
97 | """更新函数的docstring,替换屏幕分辨率占位符"""
98 | display_width_px = os.environ.get("WIDTH", "1024")
99 | display_height_px = os.environ.get("HEIGHT", "768")
100 | display_num = os.environ.get("DISPLAY_NUM", "1")
101 |
102 | if func.__doc__:
103 | func.__doc__ = func.__doc__.format(
104 | display_width_px=display_width_px,
105 | display_height_px=display_height_px,
106 | display_num=display_num
107 | )
108 | return func
109 |
110 |
111 | def base64_to_pil(base64_str):
112 | """
113 | Convert a base64 string to a PIL Image object
114 |
115 | Args:
116 | base64_str (str): The base64 string. If it contains metadata
117 | (like 'data:image/jpeg;base64,'), it will be handled.
118 |
119 | Returns:
120 | fastmcp: The fastmcp Image object
121 | """
122 | # If the base64 string includes metadata (data URI), remove it
123 | if ',' in base64_str:
124 | base64_str = base64_str.split(',')[1]
125 | img_data = base64.b64decode(base64_str)
126 | img = Image(data=img_data, format="png")
127 | return img
128 |
129 | # @mcp.tool()
130 | # @update_docstring_with_display_info
131 | # async def capture_region(ctx: Context,x: int, y: int, w: int, h: int) -> Image:
132 | # """
133 | # Capture screenshot only represents of a region of the remote desktop
134 | # - The screen's resolution is {display_width_px}x{display_height_px}.
135 | # - The display number is {display_num}
136 |
137 | # Args:
138 | # x: X coordinate (pixels from the left edge)
139 | # y: Y coordinate (pixels from the top edge)
140 | # w: Width of the region
141 | # h: Hight of the region
142 |
143 | # Returns:
144 | # Image: Screenshot of the remote desktop
145 | # """
146 | # vnc = ctx.request_context.lifespan_context.vnc
147 | # screenshot = await vnc.capture_region(x,y,w,h)
148 |
149 | # # Convert PIL Image to bytes
150 | # img_bytes = io.BytesIO()
151 | # screenshot.save(img_bytes, format="PNG")
152 |
153 | # return Image(data=img_bytes.getvalue(), format="png")
154 |
155 |
156 | @mcp.tool()
157 | @update_docstring_with_display_info
158 | async def computer( ctx: Context,
159 | action: Action_20250124,
160 | coordinate: List[int] = None,
161 | duration: int | float | None = None,
162 | scroll_direction: ScrollDirection | None = None,
163 | scroll_amount: int | None = None,
164 | text:str = None,
165 | ):
166 | """
167 | Use a mouse and keyboard to interact with a computer, and take screenshots.
168 | - This is an interface to a desktop GUI with Linux OS. You do not have access to a terminal or applications menu. You must click on desktop icons to start applications.
169 | - Some applications may take time to start or process actions, so you may need to wait and take successive screenshots to see the results of your actions. E.g. if you click on Firefox and a window doesn't open, try taking another screenshot.
170 | - The screen's resolution is {display_width_px}x{display_height_px}.
171 | - The display number is {display_num}
172 | - Whenever you intend to move the cursor to click on an element like an icon, you should consult a screenshot to determine the coordinates of the element before moving the cursor.
173 | - If you tried clicking on a program or link but it failed to load, even after waiting, try adjusting your cursor position so that the tip of the cursor visually falls on the element that you want to click.
174 | - Make sure to click any buttons, links, icons, etc with the cursor tip in the center of the element. Don't click boxes on their edges unless asked.
175 | - When you do `left_click` or `type` action, please make sure you do `mouse_move` to correct coordinates first.
176 |
177 | Args:
178 | action: The action to perform. The available actions are:
179 | * `key`: Press a key or key-combination on the keyboard.
180 | - This supports xdotool's `key` syntax.
181 | ' - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the numpad 0 key).'
182 | * `hold_key`: Hold down a key or multiple keys for a specified duration (in seconds). Supports the same syntax as `key`.
183 | * `type`: Type a string of text on the keyboard.
184 | * `cursor_position`: Get the current (x, y) pixel coordinate of the cursor on the screen.
185 | * `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on the screen.
186 | * `left_mouse_down`: Press the left mouse button.
187 | * `left_mouse_up`: Release the left mouse button.
188 | * `left_click`: Click the left mouse button at the specified (x, y) pixel coordinate on the screen. You can also include a key combination to hold down while clicking using the `text` parameter.
189 | * `left_click_drag`: Click and drag the cursor from `start_coordinate` to a specified (x, y) pixel coordinate on the screen.
190 | * `right_click`: Click the right mouse button at the specified (x, y) pixel coordinate on the screen.
191 | * `middle_click`: Click the middle mouse button at the specified (x, y) pixel coordinate on the screen.
192 | * `double_click`: Double-click the left mouse button at the specified (x, y) pixel coordinate on the screen.
193 | * `triple_click`: Triple-click the left mouse button at the specified (x, y) pixel coordinate on the screen.
194 | * `scroll`: Scroll the screen in a specified direction by a specified amount of clicks of the scroll wheel, at the specified (x, y) pixel coordinate. DO NOT use PageUp/PageDown to scroll.
195 | * `wait`: Wait for a specified duration (in seconds).
196 | * `screenshot`: Take a screenshot of the screen.
197 | coordinate: (x, y): This represents the center of the object. The x (pixels from the left edge) and y (pixels from the top edge) coordinates to move the mouse to. Required only by `action=mouse_move` and `action=left_click_drag`.
198 | duration: The duration to hold the key down for. Required only by `action=hold_key` and `action=wait`
199 | scroll_amount: The number of 'clicks' to scroll. Required only by `action=scroll`.
200 | scroll_direction: The direction to scroll the screen. Required only by `action=scroll`.
201 | start_coordinate: (x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to start the drag from. Required only by `action=left_click_drag`.
202 | text: Required only by `action=type`, `action=key`, and `action=hold_key`. Can also be used by click or scroll actions to hold down keys while clicking or scrolling.
203 |
204 | Returns: tool results
205 | """
206 |
207 | # if use NOVA model, the image need to rescale
208 | rescale = True if os.environ.get("NOVA") in [True,1,'1'] else False
209 | computer_tool = ComputerTool(ssh=ctx.request_context.lifespan_context.ssh,
210 | vnc=ctx.request_context.lifespan_context.vnc,
211 | is_nova = rescale
212 | )
213 | tool_input = dict(action=action, coordinate=coordinate, text=text,duration=duration,scroll_direction=scroll_direction,scroll_amount=scroll_amount)
214 | try:
215 | result = await computer_tool(**tool_input)
216 | except Exception as e:
217 | raise ValueError(f"{e}")
218 |
219 | if result.base64_image:
220 | return base64_to_pil(result.base64_image)
221 | else:
222 | return {'output':result.output,"error":result.error}
223 |
224 | @mcp.tool()
225 | async def bash(ctx: Context, command: str,restart: bool = None):
226 | """
227 | Run commands in a bash shell
228 | * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
229 | * You have access to a mirror of common linux and python packages via apt and pip.
230 | * State is persistent across command calls and discussions with the user.
231 | * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
232 | * Please avoid commands that may produce a very large amount of output.
233 | * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
234 |
235 | Args:
236 | command: the bash command to run. Required unless the tool is being restarted.
237 | restart: Specifying true will restart this tool. Otherwise, leave this unspecified.
238 |
239 | Returns: tool results
240 | """
241 | bash_tool = BashTool(ssh=ctx.request_context.lifespan_context.ssh)
242 | tool_input = dict(command=command, restart=restart)
243 | try:
244 | result = await bash_tool(**tool_input)
245 | except Exception as e:
246 | raise ValueError(f"{e}")
247 | return {'output':result.output,"error":result.error}
248 |
249 | @mcp.tool()
250 | async def str_replace_editor(ctx: Context,
251 | command: Command,
252 | path: str,
253 | file_text: str | None = None,
254 | view_range: list[int] | None = None,
255 | old_str: str | None = None,
256 | new_str: str | None = None,
257 | insert_line: int | None = None):
258 | """
259 | Custom editing tool for viewing, creating and editing files
260 | * State is persistent across command calls and discussions with the user
261 | * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
262 | * The `create` command cannot be used if the specified `path` already exists as a file
263 | * If a `command` generates a long output, it will be truncated and marked with ``
264 | * The `undo_edit` command will revert the last edit made to the file at `path`
265 |
266 | Notes for using the `str_replace` command:
267 | * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
268 | * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
269 | * The `new_str` parameter should contain the edited lines that should replace the `old_str`
270 |
271 | Args:
272 | command: The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.
273 | path: Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.
274 | file_text: Required parameter of `create` command, with the content of the file to be created.
275 | view_range: Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.
276 | old_str: Required parameter of `str_replace` command containing the string in `path` to replace.
277 | new_str: Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.
278 | insert_line: Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.
279 |
280 | Returns: tool results
281 | """
282 | editor_tool = EditTool(ssh=ctx.request_context.lifespan_context.ssh)
283 | tool_input = dict(command=command, path=path,file_text=file_text, view_range=view_range, old_str=old_str, new_str=new_str, insert_line=insert_line )
284 | try:
285 | result = await editor_tool(**tool_input)
286 | except Exception as e:
287 | raise ValueError(f"{e}")
288 | return {'output':result.output,"error":result.error}
289 |
290 | # Run server if executed directly
291 | if __name__ == "__main__":
292 | mcp.run()
--------------------------------------------------------------------------------
/remote_computer_use/setup_vnc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Exit script on any error
4 | set -e
5 |
6 | echo "=== Updating system packages ==="
7 | sudo apt update
8 | sudo apt upgrade -y
9 |
10 | echo "=== Installing necessary applications ==="
11 | sudo apt install -y xdotool scrot
12 | sudo add-apt-repository ppa:mozillateam/ppa -y
13 | sudo apt-get install -y --no-install-recommends libreoffice firefox-esr
14 |
15 | echo "=== Installing Xfce desktop environment ==="
16 | sudo apt install -y xfce4 xfce4-goodies
17 |
18 | echo "=== Installing TigerVNC Server ==="
19 | sudo apt install -y tigervnc-standalone-server tigervnc-common
20 |
21 | echo "=== Setting up VNC password ==="
22 | echo "Please create a VNC password when prompted"
23 | vncpasswd
24 |
25 | echo "=== Creating VNC startup file ==="
26 | mkdir -p ~/.vnc
27 | cat > ~/.vnc/xstartup << 'EOF'
28 | #!/bin/sh
29 |
30 | unset SESSION_MANAGER
31 | unset DBUS_SESSION_BUS_ADDRESS
32 |
33 | [ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup
34 | [ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
35 |
36 | export XKL_XMODMAP_DISABLE=1
37 | startxfce4
38 | EOF
39 |
40 | echo "=== Making startup file executable ==="
41 | chmod +x ~/.vnc/xstartup
42 |
43 | echo "=== Starting VNC Server ==="
44 | tigervncserver -xstartup /usr/bin/startxfce4 -SecurityTypes VncAuth,TLSVnc -geometry 1024x768 -localhost no :1
45 |
46 | echo "=== VNC Server setup complete! ==="
47 | echo "You can connect to your VNC server at $(hostname -I | awk '{print $1}'):5901"
48 |
--------------------------------------------------------------------------------
/remote_computer_use/ssh_controller.py:
--------------------------------------------------------------------------------
1 | """
2 | SSH Controller Module for Computer Use MCP Server
3 | Handles SSH connections and xdotool command execution
4 | """
5 | import asyncio
6 | import paramiko
7 | import os
8 |
9 |
10 | class SSHController:
11 | def __init__(self, host, port, username, password,pem_file,display_num=1):
12 | """
13 | Initialize SSH controller with connection parameters
14 |
15 | Args:
16 | host (str): SSH server hostname or IP
17 | port (int): SSH server port
18 | username (str): SSH username
19 | password (str): SSH password
20 | pem_file (str): ec2 pem files
21 | """
22 | self.host = host
23 | self.port = port
24 | self.username = username
25 | self.password = password
26 | self.pem_file = pem_file
27 | self.display_num = display_num
28 | self.client = None
29 |
30 | async def connect(self):
31 | """
32 | Establish SSH connection
33 |
34 | Returns:
35 | bool: True if connection successful, False otherwise
36 | """
37 | try:
38 | # Use asyncio to run the blocking SSH connection in a thread pool
39 | self.client = paramiko.SSHClient()
40 | self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
41 | if self.pem_file:
42 | private_key = paramiko.RSAKey.from_private_key_file(self.pem_file)
43 | await asyncio.to_thread(
44 | self.client.connect,
45 | self.host,
46 | port=self.port,
47 | username=self.username,
48 | password=self.password,
49 | pkey=private_key
50 | )
51 | else:
52 | await asyncio.to_thread(
53 | self.client.connect,
54 | self.host,
55 | port=self.port,
56 | username=self.username,
57 | password=self.password
58 | )
59 | return True
60 | except Exception as e:
61 | print(f"SSH connection error: {e}")
62 | return False
63 |
64 | async def disconnect(self):
65 | """Close SSH connection"""
66 | if self.client:
67 | try:
68 | await asyncio.to_thread(self.client.close)
69 | except Exception as e:
70 | print(f"SSH disconnect error: {e}")
71 | finally:
72 | self.client = None
73 |
74 | async def execute_command(self, command):
75 | """
76 | Execute command on remote server
77 |
78 | Args:
79 | command (str): Command to execute
80 |
81 | Returns:
82 | dict: Command execution result
83 | """
84 | if not self.client:
85 | success = await self.connect()
86 | if not success:
87 | return {"success": False, "error": "Failed to connect to SSH server"}
88 |
89 | try:
90 | # Execute command and get output
91 | stdin, stdout, stderr = await asyncio.to_thread(self.client.exec_command, command)
92 | output = await asyncio.to_thread(stdout.read)
93 | error = await asyncio.to_thread(stderr.read)
94 |
95 | output = output.decode() if output else ""
96 | error = error.decode() if error else ""
97 |
98 | if error:
99 | return {"success": False, "error": error, "output": output}
100 | return {"success": True, "output": output}
101 | except Exception as e:
102 | return {"success": False, "error": str(e)}
103 |
104 | async def launch_application(self, app_name):
105 | """
106 | Launch application using xdotool
107 |
108 | Args:
109 | app_name (str): Application name
110 |
111 | Returns:
112 | dict: Command execution result
113 | """
114 | # Try to activate existing window or launch new instance
115 | command = f"DISPLAY=:{self.display_num} xdotool search --name '{app_name}' windowactivate || DISPLAY=:{self.display_num} {app_name} &"
116 | return await self.execute_command(command)
117 |
118 | async def window_management(self, window_id, action):
119 | """
120 | Manage window (maximize, minimize, etc.)
121 |
122 | Args:
123 | window_id (str): Window ID or name
124 | action (str): Action to perform (maximize, minimize, etc.)
125 |
126 | Returns:
127 | dict: Command execution result
128 | """
129 | # Activate window and perform action
130 | command = f"DISPLAY=:{self.display_num} xdotool windowactivate {window_id} && DISPLAY=:{self.display_num} xdotool {action}"
131 | return await self.execute_command(command)
132 |
133 | async def list_windows(self):
134 | """
135 | List all windows
136 |
137 | Returns:
138 | dict: Command execution result with window list
139 | """
140 | command = f"DISPLAY=:{self.display_num} xdotool search --all --onlyvisible --name ''"
141 | result = await self.execute_command(command)
142 |
143 | if result["success"]:
144 | # Get window names for each window ID
145 | window_ids = result["output"].strip().split("\n")
146 | windows = []
147 |
148 | for window_id in window_ids:
149 | if window_id:
150 | name_cmd = f"DISPLAY=:{self.display_num} xdotool getwindowname {window_id}"
151 | name_result = await self.execute_command(name_cmd)
152 |
153 | if name_result["success"]:
154 | windows.append({
155 | "id": window_id,
156 | "name": name_result["output"].strip()
157 | })
158 |
159 | result["windows"] = windows
160 |
161 | return result
162 |
163 | async def get_window_info(self, window_id):
164 | """
165 | Get window information
166 |
167 | Args:
168 | window_id (str): Window ID
169 |
170 | Returns:
171 | dict: Window information
172 | """
173 | # Get window geometry
174 | geometry_cmd = f"DISPLAY=:{self.display_num} xdotool getwindowgeometry {window_id}"
175 | geometry_result = await self.execute_command(geometry_cmd)
176 |
177 | # Get window name
178 | name_cmd = f"DISPLAY=:{self.display_num} xdotool getwindowname {window_id}"
179 | name_result = await self.execute_command(name_cmd)
180 |
181 | return {
182 | "success": geometry_result["success"] and name_result["success"],
183 | "id": window_id,
184 | "name": name_result.get("output", "").strip() if name_result["success"] else "",
185 | "geometry": geometry_result.get("output", "") if geometry_result["success"] else ""
186 | }
187 |
--------------------------------------------------------------------------------
/remote_computer_use/test_connection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script for VNC and SSH connections
4 | Use this to verify your connection settings before running the MCP server
5 | """
6 | import os
7 | import sys
8 | import asyncio
9 | import argparse
10 | from dotenv import load_dotenv
11 | import paramiko
12 | from vncdotool import api
13 | import io
14 | from PIL import Image
15 | from vnc_controller import VNCController
16 | from ssh_controller import SSHController
17 |
18 |
19 | def print_success(message):
20 | """Print success message in green"""
21 | print(f"\033[92m✓ {message}\033[0m")
22 |
23 | def print_error(message):
24 | """Print error message in red"""
25 | print(f"\033[91m✗ {message}\033[0m")
26 |
27 | def print_info(message):
28 | """Print info message in blue"""
29 | print(f"\033[94m• {message}\033[0m")
30 |
31 | async def test_vnc_controller(host, port, username, password):
32 | """Test VNC controller"""
33 | vnc_controller = VNCController(host, port, username, password)
34 | vnc_success = await vnc_controller.connect()
35 | if not vnc_success:
36 | print_error("Warning: Failed to connect to VNC server on startup")
37 | print_success("VNC Controller connected successfully")
38 | image = await vnc_controller.capture_screenshot()
39 | if image :
40 | print_success("VNC Controller captured screenshot successfully")
41 | else:
42 | vnc_success = False
43 |
44 | await vnc_controller.mouse_click(327,91,1)
45 | return vnc_success
46 |
47 |
48 |
49 |
50 | async def test_ssh_connection(host, port, username, password,pem_file):
51 | """Test SSH connection and xdotool availability"""
52 | print_info(f"Testing SSH connection to {host}:{port}...")
53 | if pem_file:
54 | print_info(f"Using private key: {pem_file}")
55 | ssh_controller = SSHController(host, port, username, password, pem_file)
56 | ssh_success = await ssh_controller.connect()
57 | if not ssh_success:
58 | print_error("Warning: Failed to connect to SSH server on startup")
59 | print_success("SSH Controller connected successfully")
60 |
61 | # Check if xdotool is installed
62 | print_info("Checking if xdotool is installed...")
63 | results = await ssh_controller.execute_command("which xdotool")
64 | stdout = results.get('output')
65 | xdotool_path = stdout.strip()
66 | if xdotool_path:
67 | print_success(f"xdotool found at {xdotool_path}")
68 |
69 | # Test xdotool with DISPLAY
70 | print_info("Testing xdotool with DISPLAY environment variable...")
71 | results = await ssh_controller.execute_command("DISPLAY=:1 xdotool getmouselocation")
72 | stderr = results.get('error')
73 | output = results.get('output')
74 | if stderr:
75 | print_error(f"xdotool test failed: {stderr}")
76 | print_info("You may need to run 'xhost +' on the remote machine")
77 | ssh_success = False
78 | else:
79 | print_success(f"xdotool test successful: {output}")
80 | else:
81 | print_error("xdotool not found. Please install it with: sudo apt install xdotool")
82 | ssh_success = False
83 |
84 | await ssh_controller.disconnect()
85 |
86 | print_success("SSH connection closed")
87 | return ssh_success
88 |
89 |
90 |
91 | async def main():
92 | """Main function"""
93 | parser = argparse.ArgumentParser(description="Test VNC and SSH connections")
94 | parser.add_argument("--env", help="Path to .env file", default=".env")
95 | parser.add_argument("--vnc-only", action="store_true", help="Test only VNC connection")
96 | parser.add_argument("--ssh-only", action="store_true", help="Test only SSH connection")
97 | args = parser.parse_args()
98 |
99 | # Load environment variables from .env file
100 | if os.path.exists(args.env):
101 | load_dotenv(args.env)
102 | print_info(f"Loaded environment variables from {args.env}")
103 | else:
104 | print_info("No .env file found, using environment variables")
105 |
106 | # Get connection details
107 | vnc_host = os.environ.get("VNC_HOST")
108 | vnc_port = int(os.environ.get("VNC_PORT", "5900"))
109 | vnc_username = os.environ.get("VNC_USERNAME")
110 | vnc_password = os.environ.get("VNC_PASSWORD")
111 | ssh_port = int(os.environ.get("SSH_PORT", "22"))
112 | pem_file = os.environ.get("PEM_FILE", None)
113 |
114 | # Validate required environment variables
115 | if not vnc_host:
116 | print_error("VNC_HOST environment variable is required")
117 | return False
118 | if not vnc_password:
119 | print_error("VNC_PASSWORD environment variable is required")
120 | return False
121 | if not vnc_username:
122 | print_error("VNC_USERNAME environment variable is required")
123 | return False
124 |
125 | print_info(f"Testing connection to {vnc_host}")
126 | success = True
127 |
128 | # Test VNC connection
129 | if not args.ssh_only:
130 | vnc_success = await test_vnc_controller(vnc_host, vnc_port, vnc_username, vnc_password)
131 | success = success and vnc_success
132 |
133 | # Test SSH connection
134 | if not args.vnc_only:
135 | ssh_success = await test_ssh_connection(vnc_host, ssh_port, vnc_username, vnc_password,pem_file)
136 | success = success and ssh_success
137 |
138 | # Print summary
139 | print("\n" + "=" * 50)
140 | if success:
141 | print_success("All tests completed successfully!")
142 | print_info("You can now run the Computer Use MCP Server")
143 | else:
144 | print_error("Some tests failed. Please check the errors above.")
145 |
146 | return success
147 |
148 | if __name__ == "__main__":
149 | # Run the async main function
150 | success = asyncio.run(main())
151 | sys.exit(0 if success else 1)
152 |
--------------------------------------------------------------------------------
/remote_computer_use/tools/base.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod
2 | from dataclasses import dataclass, fields, replace
3 | from typing import Any
4 | from anthropic.types.beta import BetaToolUnionParam
5 |
6 |
7 | class BaseAnthropicTool(metaclass=ABCMeta):
8 | """Abstract base class for tools."""
9 |
10 | @abstractmethod
11 | def __call__(self, **kwargs) -> Any:
12 | """Executes the tool with the given arguments."""
13 | ...
14 |
15 | @abstractmethod
16 | def to_params(
17 | self,
18 | ) -> BetaToolUnionParam:
19 | raise NotImplementedError
20 |
21 | @abstractmethod
22 | def to_params_nova(
23 | self,
24 | ) -> BetaToolUnionParam:
25 | raise NotImplementedError
26 |
27 | @dataclass(kw_only=True, frozen=True)
28 | class ToolResult:
29 | """Represents the result of a tool execution."""
30 |
31 | output: str | None = None
32 | error: str | None = None
33 | base64_image: str | None = None
34 | system: str | None = None
35 |
36 | def __bool__(self):
37 | return any(getattr(self, field.name) for field in fields(self))
38 |
39 | def __add__(self, other: "ToolResult"):
40 | def combine_fields(
41 | field: str | None, other_field: str | None, concatenate: bool = True
42 | ):
43 | if field and other_field:
44 | if concatenate:
45 | return field + other_field
46 | raise ValueError("Cannot combine tool results")
47 | return field or other_field
48 |
49 | return ToolResult(
50 | output=combine_fields(self.output, other.output),
51 | error=combine_fields(self.error, other.error),
52 | base64_image=combine_fields(self.base64_image, other.base64_image, False),
53 | system=combine_fields(self.system, other.system),
54 | )
55 |
56 | def replace(self, **kwargs):
57 | """Returns a new ToolResult with the given fields replaced."""
58 | return replace(self, **kwargs)
59 |
60 |
61 | class CLIResult(ToolResult):
62 | """A ToolResult that can be rendered as a CLI output."""
63 |
64 |
65 | class ToolFailure(ToolResult):
66 | """A ToolResult that represents a failure."""
67 |
68 |
69 | class ToolError(Exception):
70 | """Raised when a tool encounters an error."""
71 |
72 | def __init__(self, message):
73 | self.message = message
--------------------------------------------------------------------------------
/remote_computer_use/tools/bash.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from typing import ClassVar, Literal
4 |
5 | from anthropic.types.beta import BetaToolBash20241022Param
6 |
7 | from .base import BaseAnthropicTool, CLIResult, ToolError, ToolResult
8 | from .tools_config import bash_description,bash_input_schema
9 |
10 |
11 | class _BashSession:
12 | """A session of a bash shell over SSH."""
13 |
14 | _started: bool
15 | _ssh_controller: 'SSHController'
16 |
17 | command: str = "/bin/bash"
18 | _timeout: float = 120.0 # seconds
19 |
20 | def __init__(self, ssh_controller):
21 | self._started = False
22 | self._timed_out = False
23 | self._ssh_controller = ssh_controller
24 |
25 | async def start(self):
26 | if self._started:
27 | return
28 |
29 | # Check if SSH controller is connected
30 | if not self._ssh_controller.client:
31 | success = await self._ssh_controller.connect()
32 | if not success:
33 | raise ToolError("Failed to connect to SSH server")
34 |
35 | self._started = True
36 |
37 | def stop(self):
38 | """No need to terminate the bash shell in SSH mode."""
39 | if not self._started:
40 | raise ToolError("Session has not started.")
41 | # In SSH mode, we don't terminate anything
42 | # The SSH connection is managed by SSHController
43 | pass
44 |
45 | async def run(self, command: str):
46 | """Execute a command via SSH."""
47 | if not self._started:
48 | raise ToolError("Session has not started.")
49 |
50 | if self._timed_out:
51 | raise ToolError(
52 | f"timed out: bash has not returned in {self._timeout} seconds and must be restarted",
53 | )
54 |
55 | try:
56 | # Execute the command via SSH with a timeout
57 | async with asyncio.timeout(self._timeout):
58 | result = await self._ssh_controller.execute_command(command)
59 | except asyncio.TimeoutError:
60 | self._timed_out = True
61 | raise ToolError(
62 | f"timed out: bash has not returned in {self._timeout} seconds and must be restarted",
63 | ) from None
64 |
65 | # Handle SSH connection failures or command errors
66 | if not result["success"]:
67 | if "error" in result and "Connection" in result["error"]:
68 | # SSH connection likely dropped
69 | self._started = False
70 | return CLIResult(output="", error=f"SSH connection error: {result.get('error', '')}")
71 | return CLIResult(output="", error=result.get("error", "Unknown SSH error"))
72 |
73 | output = result.get("output", "")
74 | error = result.get("error", "")
75 |
76 | # Follow the original behavior of trimming trailing newlines
77 | if output.endswith("\n"):
78 | output = output[:-1]
79 |
80 | if error and error.endswith("\n"):
81 | error = error[:-1]
82 |
83 | return CLIResult(output=output, error=error)
84 |
85 |
86 | class BashTool(BaseAnthropicTool):
87 | """
88 | A tool that allows the agent to run bash commands.
89 | The tool parameters are defined by Anthropic and are not editable.
90 | """
91 |
92 | _session: _BashSession | None
93 | name: ClassVar[Literal["bash"]] = "bash"
94 | api_type: ClassVar[Literal["bash_20250124"]] = "bash_20250124"
95 | ssh = None
96 |
97 | def __init__(self,ssh):
98 | self._session = None
99 | self.ssh = ssh
100 | super().__init__()
101 |
102 | async def __call__(
103 | self, command: str | None = None, restart: bool = False, **kwargs
104 | ):
105 | if restart:
106 | if self._session:
107 | self._session.stop()
108 | self._session = _BashSession(ssh_controller=self.ssh)
109 | await self._session.start()
110 |
111 | return ToolResult(system="tool has been restarted.")
112 |
113 | if self._session is None:
114 | self._session = _BashSession(ssh_controller=self.ssh)
115 | await self._session.start()
116 |
117 | if command is not None:
118 | return await self._session.run(command)
119 |
120 | raise ToolError("no command provided.")
121 |
122 | def to_params(self) -> BetaToolBash20241022Param:
123 | return {
124 | "type": self.api_type,
125 | "name": self.name,
126 | }
127 |
128 | def to_params_nova(self):
129 | return {
130 | "toolSpec":{
131 | "name":self.name,
132 | "description":bash_description,
133 | "inputSchema":{"json":bash_input_schema}
134 | }
135 | }
--------------------------------------------------------------------------------
/remote_computer_use/tools/computer.py:
--------------------------------------------------------------------------------
1 |
2 | import asyncio
3 | import base64
4 | import os
5 | import shlex
6 | import shutil
7 | from enum import StrEnum
8 | from pathlib import Path
9 | from typing import Literal, TypedDict,get_args
10 | from uuid import uuid4
11 | import io
12 | from anthropic.types.beta import BetaToolComputerUse20241022Param
13 | from .tools_config import computer_tool_description,computer_tool_input_schema
14 | from .base import BaseAnthropicTool, ToolError, ToolResult
15 |
16 | OUTPUT_DIR = "/tmp/outputs"
17 |
18 | TYPING_DELAY_MS = 20
19 | TYPING_GROUP_SIZE = 50
20 |
21 | Action = Literal[
22 | "key", #Action_20241022
23 | "type",
24 | "mouse_move",
25 | "left_click",
26 | "left_click_drag",
27 | "right_click",
28 | "middle_click",
29 | "double_click",
30 | "screenshot",
31 | "cursor_position"
32 | ]
33 |
34 | Action_20250124 = (
35 | Action
36 | | Literal[
37 | "left_mouse_down",
38 | "left_mouse_up",
39 | "scroll",
40 | "hold_key",
41 | "wait",
42 | "triple_click",
43 | ]
44 | )
45 |
46 | ScrollDirection = Literal["up", "down", "left", "right"]
47 |
48 |
49 | class Resolution(TypedDict):
50 | width: int
51 | height: int
52 |
53 |
54 | # sizes above XGA/WXGA are not recommended (see README.md)
55 | # scale down to one of these targets if ComputerTool._scaling_enabled is set
56 | MAX_SCALING_TARGETS: dict[str, Resolution] = {
57 | "XGA": Resolution(width=1024, height=768), # 4:3
58 | "WXGA": Resolution(width=1280, height=800), # 16:10
59 | "FWXGA": Resolution(width=1366, height=768), # ~16:9
60 | }
61 |
62 | CLICK_BUTTONS = {
63 | "left_click": 1,
64 | "right_click": 3,
65 | "middle_click": 2,
66 | "double_click": "--repeat 2 --delay 10 1",
67 | "triple_click": "--repeat 3 --delay 10 1",
68 | }
69 |
70 |
71 | class ScalingSource(StrEnum):
72 | COMPUTER = "computer"
73 | API = "api"
74 |
75 |
76 | class ComputerToolOptions(TypedDict):
77 | display_height_px: int
78 | display_width_px: int
79 | display_number: int | None
80 |
81 |
82 | def chunks(s: str, chunk_size: int) -> list[str]:
83 | return [s[i : i + chunk_size] for i in range(0, len(s), chunk_size)]
84 |
85 |
86 | class BaseComputerTool(BaseAnthropicTool):
87 | """
88 | A tool that allows the agent to interact with the screen, keyboard, and mouse of the current computer.
89 | The tool parameters are defined by Anthropic and are not editable.
90 | """
91 |
92 | name: Literal["computer"] = "computer"
93 | width: int
94 | height: int
95 | display_num: int | None
96 | is_nova:bool = False
97 | _screenshot_delay = 2.0
98 | _scaling_enabled = True
99 | ssh = None
100 | vnc = None
101 |
102 | @property
103 | def options(self) -> ComputerToolOptions:
104 | width, height = self.scale_coordinates(
105 | ScalingSource.COMPUTER, self.width, self.height
106 | )
107 | return {
108 | "display_width_px": width,
109 | "display_height_px": height,
110 | "display_number": self.display_num,
111 | }
112 |
113 | def to_params(self) -> BetaToolComputerUse20241022Param:
114 | return {"name": self.name, "type": self.api_type, **self.options}
115 |
116 | def to_params_nova(self):
117 | return {
118 | "toolSpec":{
119 | "name":self.name,
120 | "description":computer_tool_description.format(
121 | display_width_px=self.width,
122 | display_height_px=self.height,
123 | display_number=self.display_num
124 | ),
125 | "inputSchema":{"json":computer_tool_input_schema}
126 | }
127 | }
128 |
129 | def __init__(self,is_nova=False,ssh=None,vnc=None):
130 | super().__init__()
131 |
132 | self.width = int(os.getenv("WIDTH") or 1024)
133 | self.height = int(os.getenv("HEIGHT") or 768)
134 | assert self.width and self.height, "WIDTH, HEIGHT must be set"
135 | if (display_num := os.getenv("DISPLAY_NUM")) is not None:
136 | self.display_num = int(display_num)
137 | self._display_prefix = f"DISPLAY=:{self.display_num} "
138 | else:
139 | self.display_num = None
140 | self._display_prefix = ""
141 | self.is_nova = is_nova
142 | self.ssh=ssh
143 | self.vnc=vnc
144 | self.xdotool = f"{self._display_prefix}xdotool"
145 |
146 | def validate_and_get_coordinates(self, coordinate: tuple[int, int] | None = None):
147 | if not isinstance(coordinate, list) or len(coordinate) != 2:
148 | raise ToolError(f"{coordinate} must be a tuple of length 2")
149 | if not all(isinstance(i, int) and i >= 0 for i in coordinate):
150 | raise ToolError(f"{coordinate} must be a tuple of non-negative ints")
151 |
152 | return self.scale_coordinates(ScalingSource.API, coordinate[0], coordinate[1])
153 |
154 | async def __call__(
155 | self,
156 | *,
157 | action: Action,
158 | text: str | None = None,
159 | coordinate: tuple[int, int] | None = None,
160 | **kwargs,
161 | ):
162 | if action in ("mouse_move", "left_click_drag"):
163 | if coordinate is None:
164 | raise ToolError(f"coordinate is required for {action}")
165 | if text is not None:
166 | raise ToolError(f"text is not accepted for {action}")
167 |
168 | x, y = self.validate_and_get_coordinates(coordinate)
169 |
170 |
171 |
172 | if action == "mouse_move":
173 | return await self.shell(f"{self.xdotool} mousemove --sync {x} {y}")
174 | elif action == "left_click_drag":
175 | return await self.shell(
176 | f"{self.xdotool} mousedown 1 mousemove --sync {x} {y} mouseup 1"
177 | )
178 |
179 | if action in ("key", "type"):
180 | if text is None:
181 | raise ToolError(f"text is required for {action}")
182 | if coordinate is not None:
183 | raise ToolError(f"coordinate is not accepted for {action}")
184 | if not isinstance(text, str):
185 | raise ToolError(output=f"{text} must be a string")
186 |
187 | if action == "key":
188 | return await self.shell(f"{self.xdotool} key -- {text}")
189 | elif action == "type":
190 | results: list[ToolResult] = []
191 | for chunk in chunks(text, TYPING_GROUP_SIZE):
192 | cmd = f"{self.xdotool} type --delay {TYPING_DELAY_MS} -- {shlex.quote(chunk)}"
193 | results.append(await self.shell(cmd, take_screenshot=False))
194 | screenshot_base64 = (await self.screenshot()).base64_image
195 | return ToolResult(
196 | output="".join(result.output or "" for result in results),
197 | error="".join(result.error or "" for result in results),
198 | base64_image=screenshot_base64,
199 | )
200 |
201 | if action in (
202 | "left_click",
203 | "right_click",
204 | "double_click",
205 | "middle_click",
206 | "screenshot",
207 | "cursor_position",
208 | ):
209 | if text is not None:
210 | raise ToolError(f"text is not accepted for {action}")
211 | if coordinate is not None:
212 | raise ToolError(f"coordinate is not accepted for {action}")
213 |
214 | if action == "screenshot":
215 | return await self.screenshot()
216 | elif action == "cursor_position":
217 | result = await self.shell(
218 | f"{self.xdotool} getmouselocation --shell",
219 | take_screenshot=False,
220 | )
221 | output = result.output or ""
222 | x, y = self.scale_coordinates(
223 | ScalingSource.COMPUTER,
224 | int(output.split("X=")[1].split("\n")[0]),
225 | int(output.split("Y=")[1].split("\n")[0]),
226 | )
227 | return result.replace(output=f"X={x},Y={y}")
228 | else:
229 | click_arg = CLICK_BUTTONS[action]
230 | return await self.shell(f"{self.xdotool} click {click_arg}")
231 |
232 | raise ToolError(f"Invalid action: {action}")
233 |
234 | async def screenshot(self):
235 | try:
236 | screenshot = await self.vnc.capture_screenshot()
237 | except Exception as e:
238 | raise ToolError(f"Failed to take screenshot: {e}")
239 |
240 |
241 | # Convert PIL Image to bytes
242 | img_bytes = io.BytesIO()
243 | screenshot.save(img_bytes, format="PNG")
244 |
245 | img_bytes.seek(0) # 重置指针到开始位置
246 |
247 | # 转换为base64编码
248 | base64_encoded = base64.b64encode(img_bytes.getvalue())
249 | base64_string = base64_encoded.decode('utf-8')
250 |
251 | return ToolResult(output="took screeshot successfully",base64_image=base64_string)
252 |
253 | async def shell(self, command: str, take_screenshot=True) -> ToolResult:
254 | """Run a shell command and return the output, error, and optionally a screenshot."""
255 | results = await self.ssh.execute_command(command)
256 | stdout = results.get('output','')
257 | stderr = results.get('error','')
258 | base64_image = None
259 |
260 | if take_screenshot:
261 | # delay to let things settle before taking a screenshot
262 | await asyncio.sleep(self._screenshot_delay)
263 | base64_image = (await self.screenshot()).base64_image
264 |
265 | return ToolResult(output=stdout, error=stderr, base64_image=base64_image)
266 |
267 | def scale_coordinates(self, source: ScalingSource, x: int, y: int):
268 | """Scale coordinates to a target maximum resolution."""
269 | if not self._scaling_enabled:
270 | return x, y
271 |
272 |
273 | if self.is_nova:
274 | #Nova outputs fix 0-1000
275 | x_scaling_factor = 1000 / self.width
276 | y_scaling_factor = 1000 / self.height
277 | if source == ScalingSource.API:
278 | # scale up
279 | return round(x / x_scaling_factor), round(y / y_scaling_factor)
280 | # for nova don't require scale down to fixed dimesion
281 | return x, y
282 | # return round(x * x_scaling_factor), round(y * y_scaling_factor)
283 |
284 | else:
285 | ratio = self.width / self.height
286 | target_dimension = None
287 | for dimension in MAX_SCALING_TARGETS.values():
288 | # allow some error in the aspect ratio - not ratios are exactly 16:9
289 | if abs(dimension["width"] / dimension["height"] - ratio) < 0.02:
290 | if dimension["width"] < self.width:
291 | target_dimension = dimension
292 | break
293 | if target_dimension is None:
294 | return x, y
295 | x_scaling_factor = target_dimension["width"] / self.width
296 | y_scaling_factor = target_dimension["height"] / self.height
297 | if source == ScalingSource.API:
298 | if x > self.width or y > self.height:
299 | raise ToolError(f"Coordinates {x}, {y} are out of bounds")
300 | # scale up
301 | return round(x / x_scaling_factor), round(y / y_scaling_factor)
302 | # scale down
303 | return round(x * x_scaling_factor), round(y * y_scaling_factor)
304 |
305 | class ComputerTool20250124(BaseComputerTool, BaseAnthropicTool):
306 | api_type: Literal["computer_20250124"] = "computer_20250124"
307 |
308 | def to_params(self):
309 | return {"name": self.name, "type": self.api_type, **self.options}
310 |
311 | async def __call__(
312 | self,
313 | *,
314 | action: Action_20250124,
315 | text: str | None = None,
316 | coordinate: tuple[int, int] | None = None,
317 | scroll_direction: ScrollDirection | None = None,
318 | scroll_amount: int | None = None,
319 | duration: int | float | None = None,
320 | key: str | None = None,
321 | **kwargs,
322 | ):
323 | if action in ("left_mouse_down", "left_mouse_up"):
324 | if coordinate is not None:
325 | raise ToolError(f"coordinate is not accepted for {action=}.")
326 | command_parts = [
327 | self.xdotool,
328 | f"{'mousedown' if action == 'left_mouse_down' else 'mouseup'} 1",
329 | ]
330 | return await self.shell(" ".join(command_parts))
331 | if action == "scroll":
332 | if scroll_direction is None or scroll_direction not in get_args(
333 | ScrollDirection
334 | ):
335 | raise ToolError(
336 | f"{scroll_direction=} must be 'up', 'down', 'left', or 'right'"
337 | )
338 | if not isinstance(scroll_amount, int) or scroll_amount < 0:
339 | raise ToolError(f"{scroll_amount=} must be a non-negative int")
340 | mouse_move_part = ""
341 | if coordinate is not None:
342 | x, y = self.validate_and_get_coordinates(coordinate)
343 | mouse_move_part = f"mousemove --sync {x} {y}"
344 | scroll_button = {
345 | "up": 4,
346 | "down": 5,
347 | "left": 6,
348 | "right": 7,
349 | }[scroll_direction]
350 |
351 | command_parts = [self.xdotool, mouse_move_part]
352 | if text:
353 | command_parts.append(f"keydown {text}")
354 | command_parts.append(f"click --repeat {scroll_amount} {scroll_button}")
355 | if text:
356 | command_parts.append(f"keyup {text}")
357 |
358 | return await self.shell(" ".join(command_parts))
359 |
360 | if action in ("hold_key", "wait"):
361 | if duration is None or not isinstance(duration, (int, float)):
362 | raise ToolError(f"{duration=} must be a number")
363 | if duration < 0:
364 | raise ToolError(f"{duration=} must be non-negative")
365 | if duration > 100:
366 | raise ToolError(f"{duration=} is too long.")
367 |
368 | if action == "hold_key":
369 | if text is None:
370 | raise ToolError(f"text is required for {action}")
371 | escaped_keys = shlex.quote(text)
372 | command_parts = [
373 | self.xdotool,
374 | f"keydown {escaped_keys}",
375 | f"sleep {duration}",
376 | f"keyup {escaped_keys}",
377 | ]
378 | return await self.shell(" ".join(command_parts))
379 |
380 | if action == "wait":
381 | await asyncio.sleep(duration)
382 | return await self.screenshot()
383 |
384 | if action in (
385 | "left_click",
386 | "right_click",
387 | "double_click",
388 | "triple_click",
389 | "middle_click",
390 | ):
391 | if text is not None:
392 | raise ToolError(f"text is not accepted for {action}")
393 | mouse_move_part = ""
394 | if coordinate is not None:
395 | x, y = self.validate_and_get_coordinates(coordinate)
396 | mouse_move_part = f"mousemove --sync {x} {y}"
397 |
398 | command_parts = [self.xdotool, mouse_move_part]
399 | if key:
400 | command_parts.append(f"keydown {key}")
401 | command_parts.append(f"click {CLICK_BUTTONS[action]}")
402 | if key:
403 | command_parts.append(f"keyup {key}")
404 |
405 | return await self.shell(" ".join(command_parts))
406 |
407 | return await super().__call__(
408 | action=action, text=text, coordinate=coordinate, key=key, **kwargs
409 | )
--------------------------------------------------------------------------------
/remote_computer_use/tools/tools_config.py:
--------------------------------------------------------------------------------
1 | computer_tool_description = """Use a mouse and keyboard to interact with a computer, and take screenshots.
2 | - This is an interface to a desktop GUI. You do not have access to a terminal or applications menu. You must click on desktop icons to start applications.
3 | - Some applications may take time to start or process actions, so you may need to wait and take successive screenshots to see the results of your actions. E.g. if you click on Firefox and a window doesn't open, try taking another screenshot.
4 | - The screen's resolution is {display_width_px}x{display_height_px}.
5 | - The display number is {display_number}
6 | - Whenever you intend to move the cursor to click on an element like an icon, you should consult a screenshot to determine the coordinates of the element before moving the cursor.
7 | - If you tried clicking on a program or link but it failed to load, even after waiting, try adjusting your cursor position so that the tip of the cursor visually falls on the element that you want to click.
8 | - Make sure to click any buttons, links, icons, etc with the cursor tip in the center of the element. Don't click boxes on their edges unless asked.
9 | - When you do `left_click` or `type` action, please make sure you do `mouse_move` to correct coordinates first.
10 |
11 | """
12 |
13 | computer_tool_input_schema = {
14 | "properties": {
15 | "action": {
16 | "description": "The action to perform. The available actions are:\n"
17 | "* `key`: Press a key or key-combination on the keyboard.\n"
18 | " - This supports xdotool's `key` syntax.\n"
19 | ' - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the numpad 0 key).\n'
20 | "* `hold_key`: Hold down a key or multiple keys for a specified duration (in seconds). Supports the same syntax as `key`.\n"
21 | "* `type`: Type a string of text on the keyboard.\n"
22 | "* `cursor_position`: Get the current (x, y) pixel coordinate of the cursor on the screen.\n"
23 | "* `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on the screen.\n"
24 | "* `left_mouse_down`: Press the left mouse button.\n"
25 | "* `left_mouse_up`: Release the left mouse button.\n"
26 | "* `left_click`: Click the left mouse button at the specified (x, y) pixel coordinate on the screen. You can also include a key combination to hold down while clicking using the `text` parameter.\n"
27 | "* `left_click_drag`: Click and drag the cursor from `start_coordinate` to a specified (x, y) pixel coordinate on the screen.\n"
28 | "* `right_click`: Click the right mouse button at the specified (x, y) pixel coordinate on the screen.\n"
29 | "* `middle_click`: Click the middle mouse button at the specified (x, y) pixel coordinate on the screen.\n"
30 | "* `double_click`: Double-click the left mouse button at the specified (x, y) pixel coordinate on the screen.\n"
31 | "* `triple_click`: Triple-click the left mouse button at the specified (x, y) pixel coordinate on the screen.\n"
32 | "* `scroll`: Scroll the screen in a specified direction by a specified amount of clicks of the scroll wheel, at the specified (x, y) pixel coordinate. DO NOT use PageUp/PageDown to scroll.\n"
33 | "* `wait`: Wait for a specified duration (in seconds).\n"
34 | "* `screenshot`: Take a screenshot of the screen.",
35 | "enum": [
36 | "key",
37 | "hold_key",
38 | "type",
39 | "cursor_position",
40 | "mouse_move",
41 | "left_mouse_down",
42 | "left_mouse_up",
43 | "left_click",
44 | "left_click_drag",
45 | "right_click",
46 | "middle_click",
47 | "double_click",
48 | "triple_click",
49 | "scroll",
50 | "wait",
51 | "screenshot",
52 | ],
53 | "type": "string",
54 | },
55 | "coordinate": {
56 | "description": "(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to move the mouse to. Required only by `action=mouse_move` and `action=left_click_drag`.",
57 | "type": "array",
58 | },
59 | "duration": {
60 | "description": "The duration to hold the key down for. Required only by `action=hold_key` and `action=wait`.",
61 | "type": "integer",
62 | },
63 | "scroll_amount": {
64 | "description": "The number of 'clicks' to scroll. Required only by `action=scroll`.",
65 | "type": "integer",
66 | },
67 | "scroll_direction": {
68 | "description": "The direction to scroll the screen. Required only by `action=scroll`.",
69 | "enum": ["up", "down", "left", "right"],
70 | "type": "string",
71 | },
72 | "start_coordinate": {
73 | "description": "(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to start the drag from. Required only by `action=left_click_drag`.",
74 | "type": "array",
75 | },
76 | "text": {
77 | "description": "Required only by `action=type`, `action=key`, and `action=hold_key`. Can also be used by click or scroll actions to hold down keys while clicking or scrolling.",
78 | "type": "string",
79 | },
80 | },
81 | "required": ["action"],
82 | "type": "object",
83 | }
84 |
85 |
86 |
87 |
88 | text_editor_description = """Custom editing tool for viewing, creating and editing files
89 | * State is persistent across command calls and discussions with the user
90 | * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
91 | * The `create` command cannot be used if the specified `path` already exists as a file
92 | * If a `command` generates a long output, it will be truncated and marked with ``
93 | * The `undo_edit` command will revert the last edit made to the file at `path`
94 |
95 | Notes for using the `str_replace` command:
96 | * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
97 | * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
98 | * The `new_str` parameter should contain the edited lines that should replace the `old_str`
99 | """
100 |
101 | text_editor_input_schema = {
102 | "properties": {
103 | "command": {
104 | "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.",
105 | "enum": ["view", "create", "str_replace", "insert", "undo_edit"],
106 | "type": "string",
107 | },
108 | "file_text": {
109 | "description": "Required parameter of `create` command, with the content of the file to be created.",
110 | "type": "string",
111 | },
112 | "insert_line": {
113 | "description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.",
114 | "type": "integer",
115 | },
116 | "new_str": {
117 | "description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.",
118 | "type": "string",
119 | },
120 | "old_str": {
121 | "description": "Required parameter of `str_replace` command containing the string in `path` to replace.",
122 | "type": "string",
123 | },
124 | "path": {
125 | "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
126 | "type": "string",
127 | },
128 | "view_range": {
129 | "description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
130 | "items": {"type": "integer"},
131 | "type": "array",
132 | },
133 | },
134 | "required": ["command", "path"],
135 | "type": "object",
136 | }
137 |
138 |
139 | bash_description = """Run commands in a bash shell
140 | * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
141 | * You have access to a mirror of common linux and python packages via apt and pip.
142 | * State is persistent across command calls and discussions with the user.
143 | * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
144 | * Please avoid commands that may produce a very large amount of output.
145 | * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
146 |
147 | """
148 |
149 | bash_input_schema = {
150 | "properties": {
151 | "command": {
152 | "description": "The bash command to run. Required unless the tool is being restarted.",
153 | "type": "string",
154 | },
155 | "restart": {
156 | "description": "Specifying true will restart this tool. Otherwise, leave this unspecified.",
157 | "type": "boolean",
158 | },
159 | },
160 | "required": [
161 | "command"
162 | ],
163 | "type": "object"
164 | }
--------------------------------------------------------------------------------
/remote_computer_use/vnc_controller.py:
--------------------------------------------------------------------------------
1 | """
2 | VNC Controller Module for Computer Use MCP Server
3 | Handles VNC connections, screen capture, and input events
4 | """
5 | import asyncio
6 | from vncdotool import api
7 | import io
8 | from PIL import Image
9 | import tempfile
10 |
11 |
12 | class VNCController:
13 | def __init__(self, host, port, username, password):
14 | """
15 | Initialize VNC controller with connection parameters
16 |
17 | Args:
18 | host (str): VNC server hostname or IP
19 | port (int): VNC server port
20 | username (str): VNC username
21 | password (str): VNC password
22 | """
23 | self.host = host
24 | self.port = port
25 | self.username = username
26 | self.password = password
27 | self.client = None
28 |
29 | async def connect(self):
30 | """
31 | Establish VNC connection
32 |
33 | Returns:
34 | bool: True if connection successful, False otherwise
35 | """
36 | try:
37 | # Use asyncio to run the blocking VNC connection in a thread pool
38 | self.client = await asyncio.to_thread(
39 | api.connect,
40 | f"{self.host}::{self.port}",
41 | self.password
42 | )
43 | return True
44 | except Exception as e:
45 | print(f"VNC connection error: {e}")
46 | return False
47 |
48 | async def disconnect(self):
49 | """Close VNC connection"""
50 | if self.client:
51 | try:
52 | await asyncio.to_thread(self.client.disconnect)
53 | except Exception as e:
54 | print(f"VNC disconnect error: {e}")
55 | finally:
56 | self.client = None
57 |
58 | async def capture_screenshot(self):
59 | """
60 | Capture screenshot from VNC session
61 |
62 | Returns:
63 | PIL.Image: Screenshot image
64 | """
65 | if not self.client:
66 | success = await self.connect()
67 | if not success:
68 | raise Exception("Failed to connect to VNC server")
69 |
70 | try:
71 | # Capture screen and convert to PIL Image using temp file
72 | with tempfile.NamedTemporaryFile(suffix='.png', delete=True) as tmp:
73 | await asyncio.to_thread(self.client.captureScreen, tmp.name)
74 | image = Image.open(tmp.name)
75 | return image
76 | except Exception as e:
77 | raise Exception(f"Screenshot capture error: {e}")
78 |
79 |
80 | async def capture_region(self,x: int, y: int, w: int, h: int, incremental: bool = False):
81 | """
82 | Save a region of the current display to filename
83 |
84 | Returns:
85 | PIL.Image: Screenshot image
86 | """
87 | if not self.client:
88 | success = await self.connect()
89 | if not success:
90 | raise Exception("Failed to connect to VNC server")
91 |
92 | try:
93 | # Capture screen and convert to PIL Image using temp file
94 | with tempfile.NamedTemporaryFile(suffix='.png', delete=True) as tmp:
95 | await asyncio.to_thread(self.client.captureRegion, tmp.name,x,y,w,h,incremental)
96 | image = Image.open(tmp.name)
97 | return image
98 | except Exception as e:
99 | raise Exception(f"Region Screenshot capture error: {e}")
100 |
101 |
102 | async def mouse_move(self, x, y):
103 | """
104 | Move mouse to coordinates
105 |
106 | Args:
107 | x (int): X coordinate
108 | y (int): Y coordinate
109 | """
110 | if not self.client:
111 | success = await self.connect()
112 | if not success:
113 | raise Exception("Failed to connect to VNC server")
114 | try:
115 | await asyncio.to_thread(self.client.mouseMove, x, y)
116 | except Exception as e:
117 | raise Exception(f"Mouse move error: {e}")
118 |
119 |
120 | async def mouse_click(self, x, y, button=1):
121 | """
122 | Click at coordinates
123 |
124 | Args:
125 | x (int): X coordinate
126 | y (int): Y coordinate
127 | button (int): Mouse button (1=left, 2=middle, 3=right)
128 | """
129 | if not self.client:
130 | success = await self.connect()
131 | if not success:
132 | raise Exception("Failed to connect to VNC server")
133 | try:
134 | await asyncio.to_thread(self.client.mousePress, button)
135 | await asyncio.to_thread(self.client.mouseUp, button)
136 | await asyncio.to_thread(self.client.mouseUp, button)
137 | except Exception as e:
138 | raise Exception(f"Mouse click error: {e}")
139 |
140 |
141 | async def mouse_scroll(self, steps=1, direction="down"):
142 | """
143 | Scroll the mouse wheel
144 |
145 | Args:
146 | steps (int): Number of scroll steps
147 | direction (str): 'up' or 'down'
148 | """
149 | if not self.client:
150 | success = await self.connect()
151 | if not success:
152 | raise Exception("Failed to connect to VNC server")
153 |
154 | button = 4 if direction == "up" else 5 # 4 = scroll up, 5 = scroll down
155 |
156 | for _ in range(steps):
157 | try:
158 | await asyncio.to_thread(self.client.mousePress, button)
159 | await asyncio.to_thread(self.client.mouseDown, button)
160 | except Exception as e:
161 | raise Exception(f"Mouse scroll error: {e}")
162 |
163 | async def type_text(self, text):
164 | """
165 | Type text
166 |
167 | Args:
168 | text (str): Text to type
169 | """
170 | if not self.client:
171 | success = await self.connect()
172 | if not success:
173 | raise Exception("Failed to connect to VNC server")
174 |
175 | async def send_text(text):
176 | for char in text:
177 | self.client.keyPress(char)
178 |
179 | try:
180 | await asyncio.to_thread(send_text, text)
181 | except Exception as e:
182 | raise Exception(f"Text input error: {e}")
183 |
184 | async def key_press(self, key):
185 | """
186 | Press a key
187 |
188 | Args:
189 | key (str): Key to press (e.g., 'enter', 'escape', etc.)
190 | """
191 | if not self.client:
192 | success = await self.connect()
193 | if not success:
194 | raise Exception("Failed to connect to VNC server")
195 |
196 | try:
197 | await asyncio.to_thread(self.client.keyPress, key)
198 | except Exception as e:
199 | raise Exception(f"Key press error: {e}")
200 |
--------------------------------------------------------------------------------
/s3_upload_server/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/s3_upload_server/README.md:
--------------------------------------------------------------------------------
1 | # S3 Upload Server
2 |
3 | An MCP server that provides file upload functionality to AWS S3 with presigned URL access.
4 |
5 | ## Features
6 |
7 | - Uploads files to S3 bucket named `strands-agent-cn-demo-{accountId}`
8 | - Automatically creates the bucket if it doesn't exist
9 | - Uploads files to the `files/` folder within the bucket
10 | - Sets correct content type based on file extension
11 | - Returns a presigned URL with 1-hour expiration for secure access
12 | - Files remain private in S3 but accessible via presigned URL
13 |
14 | ## Prerequisites
15 |
16 | - AWS credentials configured (via AWS CLI, environment variables, or IAM roles)
17 | - ICP Recordal for AWS account (if using AWS China account) https://www.amazonaws.cn/en/about-aws/china/faqs/#do-i-need-icp-recordal
18 | - Required AWS permissions:
19 | - `sts:GetCallerIdentity`
20 | - `s3:CreateBucket`
21 | - `s3:ListBucket`
22 | - `s3:PutObject`
23 | - `s3:GetObject` (for presigned URL generation)
24 |
25 | ## Setup
26 |
27 | 1. Clone the repository to your local machine or server
28 | 2. Install dependencies:
29 | ```bash
30 | cd s3_upload_server
31 | uv sync
32 | ```
33 |
34 | 3. Add to your MCP client configuration:
35 | ```json
36 | {
37 | "mcpServers": {
38 | "s3-upload": {
39 | "command": "uv",
40 | "args": [
41 | "--directory", "/path/to/s3_upload_server",
42 | "run", "src/server.py"
43 | ],
44 | "env": {
45 | "AWS_ACCESS_KEY_ID":"your access key",
46 | "AWS_SECRET_ACCESS_KEY":"your secret acess key",
47 | "AWS_SESSION_TOKEN":"", //optional
48 | "EXPIRE_HOURS":144 // 24*7 maximum allowed
49 | }
50 | }
51 | }
52 | }
53 | ```
54 |
55 | ## Usage
56 |
57 | The server provides one tool:
58 |
59 | ### upload_file
60 |
61 | Uploads a file to S3 and returns a presigned URL with 1-hour expiration.
62 |
63 | **Parameters:**
64 | - `file_name` (string): Name of the file including extension
65 | - `file_content` (string): Content of the file as a string
66 |
67 | **Returns:**
68 | JSON object with:
69 | - `success`: Boolean indicating success/failure
70 | - `message`: Success message
71 | - `s3_url`: Presigned URL of the uploaded file (expires in 1 hour)
72 | - `bucket_name`: Name of the S3 bucket used
73 | - `account_id`: AWS account ID
74 | - `error`: Error message (if failed)
75 |
76 | **Example:**
77 | ```python
78 | # Upload a text file
79 | upload_file("hello.txt", "Hello, World!")
80 |
81 | # Upload an HTML file
82 | upload_file("index.html", "Hello ")
83 | ```
84 |
85 | ## Security Notes
86 |
87 | - Files are uploaded as private objects in S3
88 | - Access is provided through presigned URLs that expire after 1 hour
89 | - This provides better security than public objects as access is time-limited
90 | - Ensure you don't upload sensitive information that shouldn't be accessible even temporarily
91 | - Consider implementing additional access controls if needed
92 |
--------------------------------------------------------------------------------
/s3_upload_server/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "s3-upload-server"
3 | version = "0.1.0"
4 | description = "MCP server for uploading files to S3 with public access"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "mcp>=1.6.0",
9 | "boto3>=1.34.0",
10 | ]
11 |
--------------------------------------------------------------------------------
/s3_upload_server/src/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import json
3 | import mimetypes
4 | import os
5 | from typing import Optional, Dict, Any
6 | from mcp.server.fastmcp import FastMCP, Context
7 | import boto3
8 | from botocore.exceptions import ClientError, NoCredentialsError
9 | import logging
10 |
11 | # Constants
12 | DEFAULT_AWS_REGION = 'cn-northwest-1'
13 |
14 | # Required AWS permissions:
15 | # - sts:GetCallerIdentity
16 | # - s3:CreateBucket
17 | # - s3:ListBucket
18 | # - s3:PutObject
19 | # - s3:GetObject (for presigned URL generation)
20 |
21 | # Set up logging
22 | logging.basicConfig(level=logging.INFO)
23 | logger = logging.getLogger(__name__)
24 | expire_hours = int(os.environ.get('EXPIRE_HOURS',144))
25 |
26 | mcp = FastMCP("s3-upload-server")
27 |
28 | def get_aws_credentials() -> Dict[str, str]:
29 | """Get AWS credentials from environment variables"""
30 | access_key = os.environ.get('AWS_ACCESS_KEY_ID')
31 | secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
32 | session_token = os.environ.get('AWS_SESSION_TOKEN') # Optional
33 | if not access_key or not secret_key:
34 | raise ValueError("AWS credentials not found in environment variables. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.")
35 |
36 | credentials = {
37 | 'aws_access_key_id': access_key,
38 | 'aws_secret_access_key': secret_key
39 | }
40 |
41 | if session_token:
42 | credentials['aws_session_token'] = session_token
43 |
44 | return credentials
45 |
46 | def get_aws_region() -> str:
47 | """Get AWS region from environment variable or use default"""
48 | return os.environ.get('AWS_DEFAULT_REGION', os.environ.get('AWS_REGION', DEFAULT_AWS_REGION))
49 |
50 | def get_account_id(region: str = None) -> str:
51 | """Get current AWS account ID using STS"""
52 | try:
53 | credentials = get_aws_credentials()
54 | if region:
55 | sts_client = boto3.client('sts', region_name=region, **credentials)
56 | else:
57 | sts_client = boto3.client('sts', **credentials)
58 | response = sts_client.get_caller_identity()
59 | return response['Account']
60 | except NoCredentialsError:
61 | raise ValueError("AWS credentials not configured. Please configure AWS credentials.")
62 | except Exception as e:
63 | raise ValueError(f"Failed to get AWS account ID: {str(e)}")
64 |
65 | def get_content_type(file_name: str) -> str:
66 | """Get content type based on file extension"""
67 | content_type, _ = mimetypes.guess_type(file_name)
68 | return content_type or 'application/octet-stream'
69 |
70 | def create_bucket_if_not_exists(s3_client, bucket_name: str, region: str) -> bool:
71 | """Create S3 bucket if it doesn't exist"""
72 | try:
73 | # Check if bucket exists
74 | s3_client.head_bucket(Bucket=bucket_name)
75 | logger.info(f"Bucket {bucket_name} already exists")
76 | return True
77 | except ClientError as e:
78 | error_code = e.response['Error']['Code']
79 | if error_code == '404':
80 | # Bucket doesn't exist, create it
81 | try:
82 | if region == 'us-east-1':
83 | # For us-east-1, don't specify LocationConstraint
84 | s3_client.create_bucket(Bucket=bucket_name)
85 | else:
86 | s3_client.create_bucket(
87 | Bucket=bucket_name,
88 | CreateBucketConfiguration={'LocationConstraint': region}
89 | )
90 | logger.info(f"Created bucket {bucket_name}")
91 | return True
92 | except ClientError as create_error:
93 | raise ValueError(f"Failed to create bucket {bucket_name}: {str(create_error)}")
94 | else:
95 | raise ValueError(f"Failed to check bucket {bucket_name}: {str(e)}")
96 |
97 | def upload_file_to_s3(s3_client, bucket_name: str, file_name: str, file_content: str, folder: str = 'files') -> str:
98 | """Upload file to S3 and return a presigned URL with 1-hour expiration"""
99 | try:
100 | # Prepare the S3 key (file path)
101 | s3_key = f"{folder}/{file_name}"
102 |
103 | # Get content type
104 | content_type = get_content_type(file_name)
105 |
106 | # Upload file (without public ACL)
107 | s3_client.put_object(
108 | Bucket=bucket_name,
109 | Key=s3_key,
110 | Body=file_content.encode('utf-8'),
111 | ContentType=content_type
112 | )
113 |
114 | # Generate presigned URL with 1-hour expiration
115 | presigned_url = s3_client.generate_presigned_url(
116 | 'get_object',
117 | Params={'Bucket': bucket_name, 'Key': s3_key},
118 | ExpiresIn=3600*expire_hours # 7 days in seconds (maximum allowed)
119 | )
120 |
121 | logger.info(f"Successfully uploaded {file_name} and generated presigned URL")
122 | return presigned_url
123 |
124 | except ClientError as e:
125 | raise ValueError(f"Failed to upload file to S3: {str(e)}")
126 |
127 | @mcp.tool()
128 | def upload_file(file_name: str, file_content: str) -> str:
129 | """Upload a file to S3 bucket and return a presigned URL with 1-hour expiration
130 |
131 | Args:
132 | file_name: Name of the file to upload (including extension)
133 | file_content: Content of the file as a string
134 |
135 | Returns:
136 | presigned S3 URL of the uploaded file (expires in 1 hour)
137 | """
138 | try:
139 | # Get AWS credentials
140 | credentials = get_aws_credentials()
141 |
142 | # Get AWS region
143 | region = get_aws_region()
144 |
145 | # Get current AWS account ID
146 | account_id = get_account_id(region)
147 |
148 | # Create bucket name
149 | bucket_name = f"strands-agent-cn-demo-{account_id}"
150 |
151 | # Initialize S3 client with region and explicit credentials
152 | s3_client = boto3.client('s3', region_name=region, **credentials)
153 |
154 | # Create bucket if it doesn't exist
155 | create_bucket_if_not_exists(s3_client, bucket_name, region)
156 |
157 | # Upload file to S3
158 | s3_url = upload_file_to_s3(s3_client, bucket_name, file_name, file_content)
159 |
160 | return s3_url
161 |
162 | except Exception as e:
163 | logger.error(f"Error uploading file: {str(e)}")
164 | return json.dumps({
165 | "success": False,
166 | "error": str(e)
167 | }, ensure_ascii=False)
168 |
169 | if __name__ == "__main__":
170 | mcp.run()
--------------------------------------------------------------------------------
/s3_upload_server/test_server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Simple test script to verify the S3 upload server functionality
4 | """
5 | import sys
6 | import os
7 | sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
8 |
9 | from server import upload_file
10 |
11 | def test_upload():
12 | """Test the upload_file function"""
13 | print("Testing S3 upload server...")
14 |
15 | # Test with a simple text file
16 | test_content = "Hello, World! This is a test file uploaded via MCP server."
17 | test_filename = "test-file.txt"
18 |
19 | try:
20 | result = upload_file(test_filename, test_content)
21 | print("Upload result:")
22 | print(result)
23 | except Exception as e:
24 | print(f"Error during test: {str(e)}")
25 |
26 | if __name__ == "__main__":
27 | test_upload()
28 |
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 | .env
9 | # Virtual environments
10 | .venv
11 | .DS_Store
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/README.md:
--------------------------------------------------------------------------------
1 | # AWS Cognito 作为 OAuth Provider 实现的 Streamable MCP Server
2 |
3 | 本项目演示如何使用 AWS Cognito 作为 OAuth 提供者,实现一个带有身份验证的 Streamable MCP Server。
4 |
5 | ## 1. 设置 AWS Cognito
6 |
7 | ### 1.1 先决条件
8 | - 需要安装 AWS CLI 以及配置好 AWS credentials
9 |
10 | ### 1.2 修改并执行 Cognito 设置脚本
11 | #### 解释:
12 | 1. **创建用户池:**
13 | - 创建一个具有密码策略和邮箱验证的用户池
14 | - 定义基本用户属性架构
15 |
16 | 2. **配置域名:**
17 | - 为 OAuth 端点设置 Cognito 托管域名
18 | - 格式为:https://myapp-oauth-domain.auth.[region].amazoncognito.com
19 |
20 | 3. **创建资源服务器:**
21 | - 定义一个名为 "my-api" 的资源服务器,具有自定义权限范围 "read" 和 "write"
22 | - 这些权限范围将以 my-api/read 和 my-api/write 的形式提供
23 |
24 | 4. **创建应用客户端:**
25 | - 面向网页/移动应用的公共客户端:
26 | - 生成客户端密钥
27 | - 授权码和隐式流程
28 | - 可访问 OpenID 权限范围和读取权限
29 | - 面向机器对机器(M2M)的私密客户端:
30 | - 生成客户端密钥
31 | - 客户端凭证授权类型
32 | - 可访问读取和写入权限
33 |
34 | #### 执行设置脚本
35 | 根据实际情况修改后,执行如下命令:
36 | ```bash
37 | bash setup.sh us-east-1 https://<实际ip>:
38 | ```
39 |
40 | ## 2. 项目结构
41 |
42 | ```
43 | streamble_mcp_server_demo/
44 | ├── .env # 环境变量配置
45 | ├── main.py # MCP 客户端示例(支持 Cognito 认证)
46 | ├── pyproject.toml # 项目依赖
47 | ├── README.md # 项目说明
48 | ├── setup.sh # Cognito 设置脚本
49 | └── src/
50 | ├── auth.py # Flask 应用示例(用于展示 Cognito OAuth 流程)
51 | ├── cognito_auth.py # Cognito 认证工具类
52 | └── server.py # 带有 Cognito 认证的 MCP 服务器
53 | ```
54 |
55 | ## 3. 依赖安装
56 |
57 | 使用 Python 包管理器安装依赖:
58 |
59 | ```bash
60 | pip install -e .
61 | # 或者
62 | uv install
63 | ```
64 |
65 | ## 4. 运行服务器
66 |
67 | 启动带有 Cognito 认证的 MCP 服务器:
68 |
69 | ```bash
70 | python src/server.py
71 | ```
72 |
73 | 服务器将在 `http://localhost:8080` 启动,并要求所有 MCP 请求提供有效的 Cognito 访问令牌。
74 |
75 | ## 5. 测试客户端
76 |
77 | 使用客户端测试 MCP 服务器,自动获取 M2M 令牌:
78 |
79 | ```bash
80 | python main.py
81 | ```
82 |
83 | 使用自定义令牌:
84 |
85 | ```bash
86 | python main.py --token <您的访问令牌>
87 | ```
88 |
89 | ## 6. 认证流程实现
90 |
91 | ### 6.1 机器对机器认证 (M2M)
92 |
93 | 我们使用 Cognito 客户端凭证流程实现 M2M 认证:
94 |
95 | 1. 通过 `cognito_auth.py` 中的 `CognitoAuthenticator` 类获取 M2M 令牌
96 | 2. 使用 JWT 标准验证令牌
97 | 3. 向 MCP 服务器发出请求时在请求头中使用 Bearer 令牌
98 |
99 | ### 6.2 令牌验证
100 |
101 | 服务器端验证流程:
102 |
103 | 1. FastAPI 中间件拦截所有 MCP 请求
104 | 2. 提取并验证 Authorization 头中的 Bearer 令牌
105 | 3. 使用 Cognito JWKS(JSON Web Key Set)验证令牌签名和有效期
106 | 4. 验证成功后,允许请求继续处理
107 |
108 | ## 7. MCP 服务功能
109 |
110 | 本示例实现了一个简单的计算器 MCP 服务器,提供以下工具:
111 |
112 | - add: 两数相加
113 | - subtract: 相减
114 | - multiply: 相乘
115 | - divide: 相除
116 | - power: 乘方运算
117 | - square_root: 平方根计算
118 |
119 | 所有工具都需要通过有效的 Cognito 认证才能访问。
120 |
121 | ## 8. 安全注意事项
122 |
123 | - 保护 .env 文件中的客户端密钥
124 | - 在生产环境中使用 HTTPS
125 | - 定期轮换客户端密钥
126 | - 使用适当的 CORS 配置
127 | - 根据最小特权原则为不同用户分配权限范围
128 | - 验证令牌时检查权限范围、有效期和签名
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/main.py:
--------------------------------------------------------------------------------
1 | from fastmcp import Client
2 | from fastmcp.client.transports import StreamableHttpTransport
3 | import asyncio
4 | import argparse
5 | import os
6 | from dotenv import load_dotenv
7 | from src.cognito_auth import CognitoAuthenticator
8 |
9 | # Load environment variables
10 | load_dotenv()
11 |
12 | # MCP server URL
13 | http_url = "http://localhost:8080/mcp"
14 |
15 | async def use_streamable_http_client(token=None):
16 | """
17 | Connect to the MCP server using a token
18 |
19 | Args:
20 | token: Optional token to use for authentication
21 | """
22 | if token:
23 | # Use the provided token
24 | headers = {"Authorization": f"Bearer {token}"}
25 | transport = StreamableHttpTransport(url=http_url, headers=headers)
26 | client = Client(transport)
27 | else:
28 | # This will fail due to missing authentication
29 | client = Client(http_url)
30 |
31 | try:
32 | async with client:
33 | tools = await client.list_tools()
34 | print(f"Connected via Streamable HTTP, found tools: {tools}")
35 |
36 | # Try using a tool
37 | result = await client.add(a=10, b=20)
38 | print(f"10 + 20 = {result}")
39 | except Exception as e:
40 | print(f"Error connecting to MCP server: {str(e)}")
41 |
42 | async def run_with_m2m_token():
43 | """
44 | Get an M2M token from Cognito and use it to connect to the MCP server
45 | """
46 | try:
47 | # Create authenticator
48 | auth = CognitoAuthenticator()
49 |
50 | # Get M2M token
51 | print("Getting M2M token from Cognito...")
52 | token_response = auth.get_m2m_token(scopes=["my-api/read"])
53 |
54 | # Extract the access token
55 | token = token_response.get("access_token")
56 | if not token:
57 | print("Failed to get access token")
58 | return
59 |
60 | print(f"Successfully obtained access token. Token expires in {token_response.get('expires_in')} seconds")
61 |
62 | # Use the token to connect to the MCP server
63 | await use_streamable_http_client(token)
64 | except Exception as e:
65 | print(f"Error getting M2M token: {str(e)}")
66 |
67 | def parse_args():
68 | """Parse command line arguments"""
69 | parser = argparse.ArgumentParser(description="MCP Client Example")
70 | parser.add_argument("--token", help="Bearer token to use for authentication")
71 | return parser.parse_args()
72 |
73 | if __name__ == "__main__":
74 | args = parse_args()
75 |
76 | if args.token:
77 | # Use the provided token
78 | print(f"Using provided token")
79 | asyncio.run(use_streamable_http_client(args.token))
80 | else:
81 | # Get an M2M token from Cognito
82 | print("No token provided. Getting M2M token from Cognito...")
83 | asyncio.run(run_with_m2m_token())
84 |
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "streamble-mcp-server-demo"
3 | version = "0.1.0"
4 | description = "Streamable MCP server with AWS Cognito authentication"
5 | readme = "README.md"
6 | requires-python = ">=3.13"
7 | dependencies = [
8 | "fastapi>=0.115.12",
9 | "fastmcp>=2.3.0",
10 | "flask>=3.1.0",
11 | "python-dotenv>=1.1.0",
12 | "requests>=2.32.3",
13 | "requests-oauthlib>=2.0.0",
14 | "pyjwt>=2.8.0",
15 | "jwt>=1.3.1",
16 | ]
17 |
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 设置默认值
4 | REGION=${AWS_REGION:-us-east-1}
5 | DOMAIN_PREFIX="mcp-server-oauth-domain"
6 | APP_URL=${APP_URL:-"http://localhost:5000"} # 注意这里使用 localhost 而不是 127.0.0.1
7 |
8 | echo "使用AWS配置中的区域: $REGION"
9 | echo "使用默认应用URL: $APP_URL"
10 |
11 | # 创建 Cognito 用户池
12 | echo "Creating Cognito User Pool..."
13 | USER_POOL_ID=$(aws cognito-idp create-user-pool \
14 | --pool-name "MyAppUserPool" \
15 | --auto-verified-attributes email \
16 | --schema Name=email,Required=true,Mutable=true \
17 | --mfa-configuration OFF \
18 | --query 'UserPool.Id' \
19 | --output text)
20 |
21 | echo "User Pool created with ID: $USER_POOL_ID"
22 |
23 | # 设置 Cognito 域
24 | echo "Setting up Cognito domain..."
25 | aws cognito-idp create-user-pool-domain \
26 | --user-pool-id $USER_POOL_ID \
27 | --domain $DOMAIN_PREFIX
28 |
29 | echo "Domain created: https://$DOMAIN_PREFIX.auth.$REGION.amazoncognito.com"
30 |
31 | # 创建资源服务器
32 | echo "Creating Resource Server..."
33 | aws cognito-idp create-resource-server \
34 | --user-pool-id $USER_POOL_ID \
35 | --identifier "my-api" \
36 | --name "My API Server" \
37 | --scopes ScopeName=read,ScopeDescription="Read access to API" \
38 | ScopeName=write,ScopeDescription="Write access to API"
39 |
40 | echo "Resource Server created with identifier: my-api"
41 |
42 | # 公共客户端(用于浏览器/移动应用)
43 | echo "Creating Public App Client..."
44 | PUBLIC_CLIENT=$(aws cognito-idp create-user-pool-client \
45 | --user-pool-id $USER_POOL_ID \
46 | --client-name "MyPublicAppClient" \
47 | --generate-secret \
48 | --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \
49 | --allowed-o-auth-flows code implicit \
50 | --allowed-o-auth-scopes openid email profile my-api/read \
51 | --callback-urls "$APP_URL/callback" \
52 | --logout-urls "$APP_URL/logout" \
53 | --supported-identity-providers COGNITO \
54 | --prevent-user-existence-errors ENABLED \
55 | --allowed-o-auth-flows-user-pool-client)
56 |
57 | PUBLIC_CLIENT_ID=$(echo $PUBLIC_CLIENT | jq -r '.UserPoolClient.ClientId')
58 | PUBLIC_CLIENT_SECRET=$(echo $PUBLIC_CLIENT | jq -r '.UserPoolClient.ClientSecret')
59 |
60 | echo "Public App Client created with ID: $PUBLIC_CLIENT_ID"
61 | echo "Public Client Secret: $PUBLIC_CLIENT_SECRET"
62 |
63 | # 机密客户端(用于服务器到服务器)
64 | echo "Creating Confidential App Client (M2M)..."
65 | CONFIDENTIAL_CLIENT=$(aws cognito-idp create-user-pool-client \
66 | --user-pool-id $USER_POOL_ID \
67 | --client-name "MyConfidentialClient" \
68 | --generate-secret \
69 | --explicit-auth-flows ALLOW_REFRESH_TOKEN_AUTH \
70 | --allowed-o-auth-flows client_credentials \
71 | --allowed-o-auth-scopes my-api/read my-api/write \
72 | --supported-identity-providers COGNITO \
73 | --prevent-user-existence-errors ENABLED)
74 |
75 | CONFIDENTIAL_CLIENT_ID=$(echo $CONFIDENTIAL_CLIENT | jq -r '.UserPoolClient.ClientId')
76 | CONFIDENTIAL_CLIENT_SECRET=$(echo $CONFIDENTIAL_CLIENT | jq -r '.UserPoolClient.ClientSecret')
77 |
78 | echo "Confidential App Client created with ID: $CONFIDENTIAL_CLIENT_ID"
79 | echo "Client Secret: $CONFIDENTIAL_CLIENT_SECRET"
80 |
81 | # 写入环境变量到.env文件
82 | echo "Writing configuration to .env file..."
83 | cat > .env << EOF
84 | REGION=$REGION
85 | DOMAIN=$DOMAIN_PREFIX
86 | USER_POOL_ID=$USER_POOL_ID
87 | PUBLIC_CLIENT_ID=$PUBLIC_CLIENT_ID
88 | PUBLIC_CLIENT_SECRET=$PUBLIC_CLIENT_SECRET
89 | CONFIDENTIAL_CLIENT_ID=$CONFIDENTIAL_CLIENT_ID
90 | CONFIDENTIAL_CLIENT_SECRET=$CONFIDENTIAL_CLIENT_SECRET
91 | REDIRECT_URI=$APP_URL/callback
92 | LOGOUT_URI=$APP_URL/logout
93 | EOF
94 |
95 | echo "Environment variables written to .env file"
96 | echo "OAuth Server Provider setup complete!"
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/src/auth.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 | import base64
4 | import os
5 | from flask import Flask, request, redirect, url_for, session
6 | from urllib.parse import urlencode
7 | import secrets
8 | import os
9 | from dotenv import load_dotenv
10 |
11 | # 加载.env文件中的环境变量
12 | load_dotenv()
13 | def load_config_from_env():
14 | """
15 | 从环境变量中加载 OAuth 配置
16 | """
17 | # 默认配置
18 | config = {
19 | "REGION": "us-east-1", # 默认 AWS 区域
20 | "DOMAIN": "", # 默认 Cognito 域前缀
21 | "PUBLIC_CLIENT_ID": "", # 公共客户端ID
22 | "PUBLIC_CLIENT_SECRET": "", # 公共客户端密钥
23 | "CONFIDENTIAL_CLIENT_ID": "", # M2M客户端ID
24 | "CONFIDENTIAL_CLIENT_SECRET": "", # M2M客户端密钥
25 | "REDIRECT_URI": "", # 默认回调URL
26 | "LOGOUT_URI": "" # 默认注销URL
27 | }
28 |
29 | # 从环境变量更新配置
30 | for key in config.keys():
31 | env_value = os.getenv(key)
32 | if env_value:
33 | config[key] = env_value
34 |
35 | # 添加额外的环境变量(如果存在)
36 | user_pool_id = os.getenv("USER_POOL_ID")
37 | if user_pool_id:
38 | config["USER_POOL_ID"] = user_pool_id
39 |
40 | # 构建一些派生的URL
41 | config["COGNITO_DOMAIN"] = f"https://{config['DOMAIN']}.auth.{config['REGION']}.amazoncognito.com"
42 | config["AUTHORIZATION_ENDPOINT"] = f"{config['COGNITO_DOMAIN']}/oauth2/authorize"
43 | config["TOKEN_ENDPOINT"] = f"{config['COGNITO_DOMAIN']}/oauth2/token"
44 | config["USERINFO_ENDPOINT"] = f"{config['COGNITO_DOMAIN']}/oauth2/userInfo"
45 | config["LOGOUT_ENDPOINT"] = f"{config['COGNITO_DOMAIN']}/logout"
46 |
47 | return config
48 |
49 | CONFIG = load_config_from_env()
50 | print(f"Loaded OAuth configuration:{CONFIG}")
51 |
52 | # 构建 Cognito URL
53 | COGNITO_BASE_URL = f"https://{CONFIG['DOMAIN']}.auth.{CONFIG['REGION']}.amazoncognito.com"
54 | COGNITO_AUTHORIZE_URL = f"{COGNITO_BASE_URL}/oauth2/authorize"
55 | COGNITO_TOKEN_URL = f"{COGNITO_BASE_URL}/oauth2/token"
56 | COGNITO_USERINFO_URL = f"{COGNITO_BASE_URL}/oauth2/userInfo"
57 | COGNITO_LOGOUT_URL = f"{COGNITO_BASE_URL}/logout"
58 |
59 | # 初始化 Flask 应用
60 | app = Flask(__name__)
61 | app.secret_key = secrets.token_hex(16) # 用于 session 加密
62 |
63 | @app.route("/")
64 | def index():
65 | return '''
66 | Cognito OAuth 客户端演示
67 | 使用 Amazon Cognito 登录
68 | 机器对机器 API 访问演示
69 | '''
70 |
71 | @app.route("/login")
72 | def login():
73 | # 生成随机状态防止CSRF攻击
74 | state = secrets.token_hex(16)
75 | session['oauth_state'] = state
76 |
77 | print(f"登录时设置的 state: {state}")
78 | print(f"当前 session 内容: {session}")
79 |
80 | # 构建授权URL
81 | auth_params = {
82 | 'response_type': 'code',
83 | 'client_id': CONFIG['PUBLIC_CLIENT_ID'],
84 | 'redirect_uri': CONFIG['REDIRECT_URI'],
85 | 'state': state,
86 | 'scope': 'openid email profile my-api/read'
87 | }
88 | auth_url = f"{COGNITO_AUTHORIZE_URL}?{urlencode(auth_params)}"
89 |
90 | # 重定向用户到认证端点
91 | return redirect(auth_url)
92 |
93 | @app.route("/callback")
94 | def callback():
95 | received_state = request.args.get('state')
96 | session_state = session.get('oauth_state')
97 |
98 | print(f"回调收到的 state: {received_state}")
99 | print(f"会话中存储的 state: {session_state}")
100 | print(f"当前 session 内容: {session}")
101 |
102 | # 验证状态防止CSRF攻击
103 | if received_state != session_state:
104 | return f"状态不匹配,可能存在CSRF攻击。收到: {received_state},期望: {session_state}", 403
105 |
106 |
107 | # 获取授权码
108 | code = request.args.get('code')
109 | if not code:
110 | return "未收到授权码", 400
111 |
112 | # 使用授权码换取令牌
113 | token_data = {
114 | 'grant_type': 'authorization_code',
115 | 'client_id': CONFIG['PUBLIC_CLIENT_ID'],
116 | 'client_secret': CONFIG['PUBLIC_CLIENT_SECRET'],
117 | 'code': code,
118 | 'redirect_uri': CONFIG['REDIRECT_URI']
119 | }
120 |
121 | response = requests.post(COGNITO_TOKEN_URL, data=token_data)
122 |
123 | if response.status_code != 200:
124 | return f"获取令牌失败: {response.text}", 400
125 |
126 | # 保存令牌
127 | tokens = response.json()
128 | session['access_token'] = tokens.get('access_token')
129 | session['id_token'] = tokens.get('id_token')
130 | session['refresh_token'] = tokens.get('refresh_token')
131 |
132 | return redirect(url_for('profile'))
133 |
134 | @app.route("/profile")
135 | def profile():
136 | # 检查是否已认证
137 | access_token = session.get('access_token')
138 | id_token = session.get('id_token')
139 | if not access_token:
140 | return redirect(url_for('login'))
141 |
142 | # 使用访问令牌获取用户信息
143 | headers = {
144 | 'Authorization': f'Bearer {access_token}'
145 | }
146 |
147 | response = requests.get(COGNITO_USERINFO_URL, headers=headers)
148 |
149 | if response.status_code != 200:
150 | return f"获取用户信息失败: {response.text}", 400
151 |
152 | user_info = response.json()
153 |
154 | # 构建个人资料页面
155 | profile_html = f'''
156 | 用户资料
157 | 邮箱: {user_info.get('email', 'N/A')}
158 | 名称: {user_info.get('name', 'N/A')}
159 | 用户ID: {user_info.get('sub', 'N/A')}
160 |
161 | 用户信息
162 | {json.dumps(user_info, indent=4)}
163 |
164 | 访问令牌
165 | {access_token}
166 |
167 | ID令牌
168 | {id_token}
169 |
170 | 退出登录
171 | '''
172 |
173 | return profile_html
174 |
175 | @app.route("/logout")
176 | def logout():
177 | # 构建注销URL
178 | logout_params = {
179 | 'client_id': CONFIG['PUBLIC_CLIENT_ID'],
180 | 'logout_uri': CONFIG['LOGOUT_URI']
181 | }
182 |
183 | # 清除本地会话
184 | session.clear()
185 |
186 | # 重定向到Cognito的注销端点
187 | logout_url = f"{COGNITO_LOGOUT_URL}?{urlencode(logout_params)}"
188 | return redirect(logout_url)
189 |
190 | @app.route("/logout-callback")
191 | def logout_callback():
192 | return '''
193 | 已成功注销
194 | 返回首页
195 | '''
196 |
197 | @app.route("/m2m-demo")
198 | def m2m_demo():
199 | try:
200 | # 获取客户端凭证令牌
201 | token = get_m2m_token()
202 |
203 | # 使用令牌调用API示例
204 | result = call_api_with_token(token)
205 |
206 | return f'''
207 | 机器对机器 API 访问演示
208 | 成功获取令牌:
209 | {json.dumps(token, indent=4)}
210 |
211 | API 调用结果:
212 | {json.dumps(result, indent=4)}
213 |
214 | 返回首页
215 | '''
216 | except Exception as e:
217 | return f'''
218 | 机器对机器 API 访问演示
219 | 错误:
220 | {str(e)}
221 | 返回首页
222 | '''
223 |
224 | def get_m2m_token():
225 | """获取机器对机器OAuth令牌"""
226 | client_id = CONFIG['CONFIDENTIAL_CLIENT_ID']
227 | client_secret = CONFIG['CONFIDENTIAL_CLIENT_SECRET']
228 |
229 | # 构建基本认证头
230 | auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode('utf-8')).decode('utf-8')
231 |
232 | headers = {
233 | 'Authorization': f'Basic {auth_header}',
234 | 'Content-Type': 'application/x-www-form-urlencoded'
235 | }
236 |
237 | data = {
238 | 'grant_type': 'client_credentials',
239 | 'scope': 'my-api/read my-api/write'
240 | }
241 |
242 | response = requests.post(COGNITO_TOKEN_URL, headers=headers, data=data)
243 |
244 | if response.status_code != 200:
245 | raise Exception(f"获取令牌失败: {response.text}")
246 |
247 | return response.json()
248 |
249 | def call_api_with_token(token):
250 | """使用令牌调用示例API"""
251 | # 在实际应用中替换为您的真实API端点
252 | return {
253 | "success": True,
254 | "message": "成功使用M2M令牌进行API调用",
255 | "token_type": token.get("token_type"),
256 | "expires_in": token.get("expires_in"),
257 | "scope": token.get("scope")
258 | }
259 |
260 | if __name__ == "__main__":
261 | app.run(debug=True, port=5000)
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/src/cognito_auth.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import json
4 | import base64
5 | import jwt
6 | import time
7 | from typing import Dict, Optional, List
8 | from dotenv import load_dotenv
9 |
10 | # Load environment variables
11 | load_dotenv()
12 |
13 | class CognitoAuthenticator:
14 | def __init__(self):
15 | """Initialize the Cognito authenticator with configuration from environment variables"""
16 | self.config = self._load_config_from_env()
17 | self.jwks = None
18 | self.jwks_last_updated = 0
19 | self._load_jwks()
20 |
21 | def _load_config_from_env(self) -> Dict[str, str]:
22 | """Load Cognito configuration from environment variables"""
23 | config = {
24 | "REGION": os.getenv("REGION", "us-east-1"),
25 | "USER_POOL_ID": os.getenv("USER_POOL_ID", ""),
26 | "DOMAIN": os.getenv("DOMAIN", ""),
27 | "CONFIDENTIAL_CLIENT_ID": os.getenv("CONFIDENTIAL_CLIENT_ID", ""),
28 | "CONFIDENTIAL_CLIENT_SECRET": os.getenv("CONFIDENTIAL_CLIENT_SECRET", ""),
29 | "PUBLIC_CLIENT_ID": os.getenv("PUBLIC_CLIENT_ID", "")
30 | }
31 |
32 | # Build derived URLs
33 | config["COGNITO_DOMAIN"] = f"https://{config['DOMAIN']}.auth.{config['REGION']}.amazoncognito.com"
34 | config["TOKEN_ENDPOINT"] = f"{config['COGNITO_DOMAIN']}/oauth2/token"
35 | config["JWKS_URI"] = f"https://cognito-idp.{config['REGION']}.amazonaws.com/{config['USER_POOL_ID']}/.well-known/jwks.json"
36 |
37 | return config
38 |
39 | def _load_jwks(self) -> None:
40 | """Load JSON Web Key Set (JWKS) from Cognito for token validation"""
41 | # Only refresh JWKS every 24 hours
42 | current_time = time.time()
43 | if self.jwks and (current_time - self.jwks_last_updated < 86400): # 24 hours
44 | return
45 |
46 | try:
47 | jwks_url = self.config["JWKS_URI"]
48 | response = requests.get(jwks_url)
49 | if response.status_code == 200:
50 | self.jwks = response.json()
51 | self.jwks_last_updated = current_time
52 | print(f"Successfully loaded JWKS from {jwks_url}")
53 | else:
54 | print(f"Failed to load JWKS: {response.status_code} - {response.text}")
55 | except Exception as e:
56 | print(f"Error loading JWKS: {str(e)}")
57 |
58 | def get_key(self, kid: str) -> Optional[Dict]:
59 | """
60 | Get the public key that matches the key ID from the JWKS
61 | """
62 | if not self.jwks:
63 | self._load_jwks()
64 |
65 | if not self.jwks:
66 | return None
67 |
68 | for key in self.jwks.get('keys', []):
69 | if key.get('kid') == kid:
70 | return key
71 | return None
72 |
73 | def validate_token(self, token: str) -> Dict:
74 | """
75 | Validate the JWT token and return the claims if valid
76 |
77 | Args:
78 | token: The JWT token to validate
79 |
80 | Returns:
81 | The decoded claims if the token is valid
82 |
83 | Raises:
84 | Exception: If the token is invalid
85 | """
86 | # Get the header to extract the key ID (kid)
87 | try:
88 | header = jwt.get_unverified_header(token)
89 | kid = header.get('kid')
90 |
91 | if not kid:
92 | raise Exception("Token header missing 'kid'")
93 |
94 | # Get the key from JWKS
95 | key = self.get_key(kid)
96 | if not key:
97 | raise Exception(f"No matching key found for kid: {kid}")
98 |
99 | # Construct the public key in PEM format
100 | n = base64.urlsafe_b64decode(key['n'] + '=' * (4 - len(key['n']) % 4))
101 | e = base64.urlsafe_b64decode(key['e'] + '=' * (4 - len(key['e']) % 4))
102 |
103 | # Verify and decode the token
104 | claims = jwt.decode(
105 | token,
106 | key,
107 | algorithms=['RS256'],
108 | audience=self.config["PUBLIC_CLIENT_ID"],
109 | options={"verify_signature": False} # We're using a simplified validation approach
110 | )
111 |
112 | # Verify additional claims
113 | current_time = int(time.time())
114 | if claims.get('exp') and current_time > claims['exp']:
115 | raise Exception("Token has expired")
116 |
117 | if claims.get('nbf') and current_time < claims['nbf']:
118 | raise Exception("Token not yet valid")
119 |
120 | return claims
121 |
122 | except jwt.InvalidTokenError as e:
123 | raise Exception(f"Invalid token: {str(e)}")
124 | except Exception as e:
125 | raise Exception(f"Error validating token: {str(e)}")
126 |
127 | def extract_token_from_header(self, auth_header: Optional[str]) -> Optional[str]:
128 | """
129 | Extract the JWT token from the Authorization header
130 |
131 | Args:
132 | auth_header: The Authorization header value
133 |
134 | Returns:
135 | The token if found, None otherwise
136 | """
137 | if not auth_header:
138 | return None
139 |
140 | parts = auth_header.split()
141 | if len(parts) != 2 or parts[0].lower() != 'bearer':
142 | return None
143 |
144 | return parts[1]
145 |
146 | def get_m2m_token(self, scopes: List[str] = None) -> Dict:
147 | """
148 | Get a machine-to-machine token using client credentials flow
149 |
150 | Args:
151 | scopes: List of scopes to request
152 |
153 | Returns:
154 | The token response dictionary
155 | """
156 | if scopes is None:
157 | scopes = ['my-api/read']
158 |
159 | client_id = self.config['CONFIDENTIAL_CLIENT_ID']
160 | client_secret = self.config['CONFIDENTIAL_CLIENT_SECRET']
161 |
162 | # Build basic auth header
163 | auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode('utf-8')).decode('utf-8')
164 |
165 | headers = {
166 | 'Authorization': f'Basic {auth_header}',
167 | 'Content-Type': 'application/x-www-form-urlencoded'
168 | }
169 |
170 | data = {
171 | 'grant_type': 'client_credentials',
172 | 'scope': ' '.join(scopes)
173 | }
174 |
175 | response = requests.post(self.config['TOKEN_ENDPOINT'], headers=headers, data=data)
176 |
177 | if response.status_code != 200:
178 | raise Exception(f"Failed to get token: {response.text}")
179 |
180 | return response.json()
181 |
--------------------------------------------------------------------------------
/streamble_mcp_server_demo/src/server.py:
--------------------------------------------------------------------------------
1 | from fastmcp import FastMCP
2 | from fastapi import Request, HTTPException, Depends
3 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
4 | from typing import Optional, Dict
5 | import os
6 |
7 | from .cognito_auth import CognitoAuthenticator
8 |
9 | # Initialize the Cognito authenticator
10 | auth = CognitoAuthenticator()
11 |
12 | # Security scheme for Bearer token authentication
13 | security = HTTPBearer()
14 |
15 | # Authentication dependency
16 | async def validate_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict:
17 | """
18 | Validate the access token from the Authorization header
19 | """
20 | token = credentials.credentials
21 | try:
22 | # Validate the token with Cognito
23 | claims = auth.validate_token(token)
24 | return claims
25 | except Exception as e:
26 | raise HTTPException(
27 | status_code=401,
28 | detail=f"Invalid authentication token: {str(e)}",
29 | headers={"WWW-Authenticate": "Bearer"},
30 | )
31 |
32 | # Create a calculator MCP server with Cognito authentication
33 | mcp = FastMCP(
34 | name="CalculatorApp",
35 | instructions="""
36 | This is a calculator service that can perform basic arithmetic operations.
37 | Available operations:
38 | - Addition: Add two numbers together
39 | - Subtraction: Subtract one number from another
40 | - Multiplication: Multiply two numbers together
41 | - Division: Divide one number by another
42 | - Power: Raise a number to a power
43 | - Square Root: Calculate the square root of a number
44 |
45 | Please provide the numbers and specify which operation you want to perform.
46 |
47 | Authentication is required using a valid AWS Cognito token in the Authorization header.
48 | """,
49 | )
50 |
51 | # Middleware to validate authentication for all requests
52 | @mcp.app.middleware("http")
53 | async def auth_middleware(request: Request, call_next):
54 | """
55 | Middleware to validate authentication for all requests except OPTIONS requests
56 | """
57 | if request.method == "OPTIONS":
58 | # Allow preflight requests without authentication
59 | return await call_next(request)
60 |
61 | # Check if the request path is for MCP
62 | if not request.url.path.startswith("/mcp"):
63 | # For non-MCP endpoints, bypass authentication
64 | return await call_next(request)
65 |
66 | # Extract and validate the token
67 | auth_header = request.headers.get("Authorization")
68 | if not auth_header:
69 | raise HTTPException(
70 | status_code=401,
71 | detail="Missing authorization header",
72 | headers={"WWW-Authenticate": "Bearer"},
73 | )
74 |
75 | token = auth.extract_token_from_header(auth_header)
76 | if not token:
77 | raise HTTPException(
78 | status_code=401,
79 | detail="Invalid authorization header format",
80 | headers={"WWW-Authenticate": "Bearer"},
81 | )
82 |
83 | try:
84 | # Validate the token with Cognito
85 | auth.validate_token(token)
86 | # If validation succeeds, proceed with the request
87 | return await call_next(request)
88 | except Exception as e:
89 | raise HTTPException(
90 | status_code=401,
91 | detail=f"Authentication failed: {str(e)}",
92 | headers={"WWW-Authenticate": "Bearer"},
93 | )
94 |
95 | @mcp.tool()
96 | def add(a: float, b: float) -> float:
97 | """Adds two numbers together."""
98 | return a + b
99 |
100 | @mcp.tool()
101 | def subtract(a: float, b: float) -> float:
102 | """Subtracts b from a."""
103 | return a - b
104 |
105 | @mcp.tool()
106 | def multiply(a: float, b: float) -> float:
107 | """Multiplies two numbers together."""
108 | return a * b
109 |
110 | @mcp.tool()
111 | def divide(a: float, b: float) -> float:
112 | """Divides a by b. Returns an error if b is zero."""
113 | if b == 0:
114 | raise ValueError("Cannot divide by zero")
115 | return a / b
116 |
117 | @mcp.tool()
118 | def power(base: float, exponent: float) -> float:
119 | """Raises base to the power of exponent."""
120 | return base ** exponent
121 |
122 | @mcp.tool()
123 | def square_root(number: float) -> float:
124 | """Calculates the square root of a number."""
125 | if number < 0:
126 | raise ValueError("Cannot calculate square root of a negative number")
127 | return number ** 0.5
128 |
129 | if __name__ == "__main__":
130 | print(f"Starting calculator service with Cognito authentication...")
131 | mcp.run(
132 | transport="streamable-http",
133 | host="0.0.0.0",
134 | port=8080,
135 | log_level="debug",
136 | cors_origins=["*"], # Adjust this in production
137 | )
138 |
--------------------------------------------------------------------------------
/time_server/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/time_server/README.md:
--------------------------------------------------------------------------------
1 | ## Time server
2 | 通过这个mcp server让Agent知道当前时间,使用方式:
3 | git clone repo到本地或者服务器上:
4 | 在MCP clien中添加
5 | ```json
6 | {"mcpServers":
7 | { "time":
8 | { "command": "uv",
9 | "args": ["--directory","/path/to/time_server/src",
10 | "run",
11 | "server.py"]
12 | }
13 | }
14 | }
15 | ```
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/time_server/hello.py:
--------------------------------------------------------------------------------
1 | def main():
2 | print("Hello from timer-server!")
3 |
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
--------------------------------------------------------------------------------
/time_server/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "timer-server"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "mcp>=1.6.0",
9 | ]
10 |
--------------------------------------------------------------------------------
/time_server/src/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import json
3 | from typing import Optional, Dict, Any, List
4 | from mcp.server.fastmcp import FastMCP, Context
5 | from datetime import datetime, timedelta
6 | from zoneinfo import ZoneInfo
7 |
8 | mcp = FastMCP("timer-server")
9 |
10 | def get_local_tz(local_tz_override: str | None = None) -> ZoneInfo:
11 |
12 | # Get local timezone from datetime.now()
13 | tzinfo = datetime.now().astimezone(tz=None).tzinfo
14 | if tzinfo is not None:
15 | tz_str = str(tzinfo)
16 | if tz_str == "CST":
17 | tz_str = "America/Chicago"
18 |
19 | return ZoneInfo(tz_str)
20 | else:
21 | raise ValueError('get local timezone failed')
22 |
23 |
24 | def get_zoneinfo(timezone_name: str) -> ZoneInfo:
25 | try:
26 | return ZoneInfo(timezone_name)
27 | except Exception as e:
28 | raise ValueError(f"Invalid timezone: {str(e)}")
29 |
30 | def update_docstring_with_info(func):
31 | """更新函数的docstring,"""
32 | local_tz = str(get_local_tz())
33 |
34 | if func.__doc__:
35 | func.__doc__ = func.__doc__.format(
36 | local_tz=local_tz
37 | )
38 | return func
39 |
40 | @mcp.tool()
41 | @update_docstring_with_info
42 | def get_current_time(timezone_name: str) -> str:
43 | """Get current time in specified timezone
44 |
45 | Args:
46 | timezone_name: IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '{local_tz}' as local timezone if no timezone provided by the user."
47 |
48 | """
49 | timezone = get_zoneinfo(timezone_name)
50 | current_time = datetime.now(timezone)
51 |
52 | return json.dumps(dict(
53 | timezone=timezone_name,
54 | datetime=current_time.isoformat(timespec="seconds"),
55 | is_dst=bool(current_time.dst()),
56 | ),ensure_ascii=False)
57 |
58 |
59 | if __name__ == "__main__":
60 | mcp.run()
61 | # print(get_current_time("Asia/Shanghai"))
62 |
--------------------------------------------------------------------------------