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
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:
50
examples/integration/alibabacloud_api_mcp/README.md
Normal file
50
examples/integration/alibabacloud_api_mcp/README.md
Normal 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.
|
||||
101
examples/integration/alibabacloud_api_mcp/main.py
Normal file
101
examples/integration/alibabacloud_api_mcp/main.py
Normal 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())
|
||||
266
examples/integration/alibabacloud_api_mcp/oauth_handler.py
Normal file
266
examples/integration/alibabacloud_api_mcp/oauth_handler.py
Normal 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
|
||||
Reference in New Issue
Block a user