chore: initialize sandbox and overwrite remote content
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
codex-bot
2026-03-02 22:32:27 +08:00
commit a64378956a
584 changed files with 93604 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# Connect AlibabaCloud API MCP Server Example
## What This Example Demonstrates
This use case shows how to use OAuth login in agentscope to connect to the Alibaba Cloud API MCP server.
Alibaba Cloud is a world-leading cloud computing and artificial intelligence technology company, committed to providing one-stop cloud computing services and middleware for enterprises and developers.
Alibaba Cloud API MCP Server provides MCP-based access to nearly all of Alibaba Cloud's OpenAPIs. You can create and optimize them without coding at <https://api.aliyun.com/mcp>.
For example, you can add the ECS service's price query interfaces DescribePrice and CreateInstance, DescribeImages to a custom MCP service. This allows you to obtain a remote MCP address without any code configuration. Using the agent scope, you can query prices and place orders from the agent.In addition to supporting atomic OpenAPI, it also supports encapsulating Terraform HCL as a remote tool to achieve deterministic orchestration.
After adding the sample MCP, you can use queries similar to the following:
1. Find the lowest-priced ECS instance in the Hangzhou region;
2. Create an instance with the lowest price and lowest specifications in Hangzhou.
## Prerequisites
- Python 3.10 or higher
- Python package asyncio, webbrowser
- Node.js and npm (for the MCP server)
- AlibabaCloud API MCP Server connect address [Alibaba Cloud API MCP Server console](https://api.aliyun.com/mcp)
## How to Run This Example
**Edit main.py**
```python
# openai base
# read from .env
load_dotenv()
server_url = "https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/****/id/KXy******/mcp"
```
You need to create your own MCP SERVER from https://api.aliyun.com/mcp and replace the link here. Please choose an address that uses the streamable HTTP protocol.
**Run the script**:
```bash
python main.py
```
## Video example
<https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250911/otcfsk/AgentScope+%E9%9B%86%E6%88%90+OpenAPI+MCP+Server%28%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%88%9B%E5%BB%BA+ECS%29.mp4>
This video demonstrates how to complete the configuration in the agent scope using the Alibaba Cloud API MCP SERVER service. After logging in through OAuth, users can create an ECS instance using natural language.

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
"""The main entry point of the ReAct agent example."""
import asyncio
import os
from dotenv import load_dotenv
from mcp.client.auth import (
OAuthClientProvider,
)
from mcp.shared.auth import (
OAuthClientMetadata,
)
from pydantic import AnyUrl
from oauth_handler import (
InMemoryTokenStorage,
handle_callback,
handle_redirect,
)
from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.mcp import HttpStatelessClient
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
load_dotenv()
# Fetch the MCP endpoint from https://api.aliyun.com/mcp after provisioning.
server_url = (
"https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/"
"****/id/KXy******/mcp"
)
memory_token_storage = InMemoryTokenStorage()
oauth_provider = OAuthClientProvider(
server_url=server_url,
client_metadata=OAuthClientMetadata(
client_name="AgentScopeExampleClient",
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
scope=None,
),
storage=memory_token_storage,
redirect_handler=handle_redirect,
callback_handler=handle_callback,
)
stateless_client = HttpStatelessClient(
# Name used to identify the MCP
name="mcp_services_stateless",
transport="streamable_http",
url=server_url,
auth=oauth_provider,
)
def require_env_var(name: str) -> str:
"""Return the value of *name* or raise a helpful error."""
value = os.environ.get(name)
if value is None:
raise RuntimeError(f"Environment variable '{name}' must be set.")
return value
async def main() -> None:
"""The main entry point for the ReAct agent example."""
toolkit = Toolkit()
await toolkit.register_mcp_client(stateless_client)
agent = ReActAgent(
name="AlibabaCloudOpsAgent",
sys_prompt=(
"You are an Alibaba Cloud operations assistant. "
"Use ECS, RDS, VPC, and other services to satisfy requests."
),
model=DashScopeChatModel(
api_key=require_env_var("DASHSCOPE_API_KEY"),
model_name="qwen3-max-preview",
enable_thinking=False,
stream=True,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
user = UserAgent("User")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
asyncio.run(main())

View File

@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
"""OAuth handler utilities for the Alibaba Cloud MCP example."""
from __future__ import annotations
import asyncio
import socket
import threading
import webbrowser
from functools import partial
from http.server import BaseHTTPRequestHandler, HTTPServer
from textwrap import dedent
from urllib.parse import parse_qs, urlparse
from mcp.client.auth import TokenStorage
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
SUCCESS_PAGE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Authorization Complete</title>
</head>
<body>
<h1>Authorization Complete</h1>
<p>You can now return to the application.</p>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
ERROR_PAGE_TEMPLATE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Authorization Error</title>
</head>
<body>
<h1>Authorization Error</h1>
<p><strong>Code:</strong> {error}</p>
<p><strong>Description:</strong> {description}</p>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
INTERNAL_ERROR_TEMPLATE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server Error</title>
</head>
<body>
<h1>Server Error</h1>
<p>Sorry, something went wrong while handling the callback.</p>
<pre>{details}</pre>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
class InMemoryTokenStorage(TokenStorage):
"""Demo in-memory token storage implementation."""
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
"""Get stored tokens."""
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
"""Store tokens."""
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
"""Get stored client information."""
return self.client_info
async def set_client_info(
self,
client_info: OAuthClientInformationFull,
) -> None:
"""Store client information."""
self.client_info = client_info
class CallbackHandler(BaseHTTPRequestHandler):
"""HTTP handler for OAuth callback."""
def __init__(
self,
callback_server: "CallbackServer",
request: socket.socket,
client_address: tuple[str, int],
server: HTTPServer,
) -> None:
self.callback_server: "CallbackServer" = callback_server
super().__init__(request, client_address, server)
def do_GET(self) -> None:
"""Handle GET request for OAuth callback."""
try:
parsed_url = urlparse(self.path)
params = parse_qs(parsed_url.query)
if "code" in params:
code = params["code"][0]
state = params.get("state", [None])[0]
self.callback_server.auth_code = code
self.callback_server.auth_state = state
self.callback_server.auth_received = True
self.send_response(200)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(SUCCESS_PAGE.encode("utf-8"))
elif "error" in params:
error = params["error"][0]
description = params.get(
"error_description",
["Unknown error"],
)[0]
self.callback_server.auth_error = f"{error}: {description}"
self.callback_server.auth_received = True
self.send_response(400)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
page = ERROR_PAGE_TEMPLATE.format(
error=error,
description=description,
)
self.wfile.write(page.encode("utf-8"))
except Exception as exc: # pylint: disable=broad-exception-caught
self.callback_server.auth_error = str(exc)
self.callback_server.auth_received = True
self.send_response(500)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
page = INTERNAL_ERROR_TEMPLATE.format(details=exc)
self.wfile.write(page.encode("utf-8"))
class CallbackServer:
"""OAuth callback server."""
def __init__(self, port: int = 3000) -> None:
self.port = port
self.server: HTTPServer | None = None
self.thread: threading.Thread | None = None
self.auth_code: str | None = None
self.auth_state: str | None = None
self.auth_error: str | None = None
self.auth_received = False
def start(self) -> None:
"""Start callback server."""
handler = partial(CallbackHandler, self)
self.server = HTTPServer(("localhost", self.port), handler)
self.thread = threading.Thread(
target=self.server.serve_forever,
daemon=True,
)
self.thread.start()
print(f"OAuth callback server started, listening on port {self.port}")
def stop(self) -> None:
"""Stop callback server."""
if self.server:
self.server.shutdown()
self.server.server_close()
if self.thread:
self.thread.join(timeout=1)
print("OAuth callback server stopped")
async def wait_for_callback(
self,
timeout: float = 300,
) -> tuple[str, str | None]:
"""Wait for OAuth callback."""
loop = asyncio.get_running_loop()
start_time = loop.time()
while not self.auth_received:
if loop.time() - start_time > timeout:
raise TimeoutError("OAuth callback timeout")
await asyncio.sleep(0.1)
if self.auth_error:
raise RuntimeError(
f"OAuth authorization failed: {self.auth_error}",
)
if self.auth_code is None:
raise RuntimeError(
"OAuth authorization failed: missing authorization code",
)
return self.auth_code, self.auth_state
# Global callback server instance
_callback_server: CallbackServer | None = None
async def handle_redirect(auth_url: str) -> None:
"""Automatically open browser for OAuth authorization."""
global _callback_server
# Start callback server
if _callback_server is None:
_callback_server = CallbackServer(port=3000)
_callback_server.start()
print("Opening browser for OAuth authorization...")
print(f"Authorization URL: {auth_url}")
# Automatically open browser
webbrowser.open(auth_url)
async def handle_callback() -> tuple[str, str | None]:
"""Automatically handle OAuth callback."""
global _callback_server
if _callback_server is None:
raise RuntimeError("Callback server not started")
print("Waiting for OAuth authorization to complete...")
try:
# Wait for callback
code, state = await _callback_server.wait_for_callback()
print("OAuth authorization successful!")
return code, state
except Exception as e: # pylint: disable=broad-exception-caught
print(f"OAuth authorization failed: {e}")
raise
finally:
# Clean up server state but keep server running for reuse
_callback_server.auth_code = None
_callback_server.auth_state = None
_callback_server.auth_error = None
_callback_server.auth_received = False