Skip to content

Commit 8beaf5f

Browse files
authored
Merge pull request #650 from gargnipungarg/main
MCP Client code for inspector and ADK
2 parents af9e2d5 + bed4175 commit 8beaf5f

File tree

7 files changed

+379
-0
lines changed

7 files changed

+379
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Description
2+
OCI Generative AI Agents is a fully managed service that combines the power of large language models (LLMs) with AI technologies to create intelligent virtual agents that can provide personalized, context-aware, and highly engaging customer experiences.
3+
4+
The OCI Agent Development Kit (ADK) is a client-side library that simplifies building agentic applications on top of OCI Generative AI Agents Service.
5+
When pairing the ADK with OCI Generative AI Agents Service, you can use simple but powerful primitives to build complex, production-grade agentic applications.
6+
7+
In this document, we will use mcp tool integration of [ADK library](https://agents.oraclecorp.com/adk/examples/agent-mcp-tool) to connect to remote hosted agent on model deployment service.
8+
9+
# Prerequisites
10+
11+
- Install pip dependencies mentioned in [requirements.txt](./requirements.txt)
12+
```
13+
pip install -r requirements.txt
14+
```
15+
16+
- Create OCI session and populate your local OCI config files with valid [session details](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/clitoken.htm)
17+
```
18+
oci session authenticate
19+
```
20+
21+
- Start [mcp_client.py](./mcp_client.py)
22+
```
23+
python3 mcp_client.py
24+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import asyncio
2+
import requests
3+
import oci
4+
from mcp.client.session_group import StreamableHttpParameters
5+
from oracle.adk import Agent, AgentClient
6+
from oracle.adk.mcp import MCPClientStreamableHttp
7+
8+
MCP_SERVER_URL = <MODEL_DEPLOYMENT_URL>
9+
GENAI_ENDPOINT = "<ocid1.genaiagentendpoint.oc....>"
10+
def get_auth():
11+
PROFILE_NAME = 'DEFAULT'
12+
SECURITY_TOKEN_FILE_KEY = 'security_token_file'
13+
KEY_FILE_KEY = 'key_file'
14+
config = oci.config.from_file(profile_name=PROFILE_NAME)
15+
token_file = config[SECURITY_TOKEN_FILE_KEY]
16+
token = None
17+
with open(token_file, 'r') as f:
18+
token = f.read()
19+
private_key = oci.signer.load_private_key_from_file(config[KEY_FILE_KEY])
20+
signer = oci.auth.signers.SecurityTokenSigner(token, private_key, body_headers=["content-type", "x-content-sha256"])
21+
return signer
22+
23+
def oci_auth_headers():
24+
signer=get_auth()
25+
request = requests.Request("POST", MCP_SERVER_URL, auth=signer, headers={'Content-Type': 'application/json'})
26+
prepared = request.prepare()
27+
signer(prepared)
28+
del(prepared.headers['content-length'])
29+
return prepared.headers
30+
31+
async def main():
32+
33+
headers = oci_auth_headers()
34+
client = AgentClient(
35+
auth_type="security_token",
36+
profile="DEFAULT",
37+
region="us-ashburn-1",
38+
)
39+
40+
params = StreamableHttpParameters(
41+
url=MCP_SERVER_URL,
42+
headers=headers
43+
)
44+
45+
async with MCPClientStreamableHttp(
46+
params=params,
47+
name="Streamable MCP Server",
48+
) as mcp_client:
49+
agent = Agent(
50+
client=client,
51+
agent_endpoint_id=GENAI_ENDPOINT,
52+
instructions="Use the tools to answer the questions.",
53+
tools=[await mcp_client.as_toolkit()],
54+
)
55+
agent.setup()
56+
print("Done", mcp_client, agent)
57+
#mcp_client.list_tools()
58+
59+
if __name__ == "__main__":
60+
asyncio.run(main())
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Oracle Agent Development Kit - Core package
2+
oci[adk]
3+
4+
# OCI SDK for authentication and cloud services
5+
oci>=2.100.0
6+
7+
# WebSocket support for MCP connections
8+
websockets>=11.0
9+
10+
# Async HTTP client for additional API calls
11+
aiohttp>=3.8.0
12+
13+
# JSON schema validation for MCP messages
14+
jsonschema>=4.17.0
15+
16+
# Certificate handling and SSL support
17+
cryptography>=40.0.0
18+
19+
# Configuration file handling
20+
pyyaml>=6.0
21+
22+
# Logging and debugging
23+
structlog>=22.3.0
24+
25+
# HTTP/HTTPS client with connection pooling
26+
httpx>=0.24.0
27+
28+
# URL parsing and validation
29+
yarl>=1.8.0
30+
31+
# Retry logic and backoff strategies
32+
tenacity>=8.2.0
33+
34+
# Type hints support for Python < 3.9
35+
typing-extensions>=4.5.0
36+
37+
# Date/time handling
38+
python-dateutil>=2.8.2
39+
40+
# Environment variable management
41+
python-dotenv>=1.0.0
42+
43+
# Development and testing dependencies (optional)
44+
pytest>=7.2.0
45+
pytest-asyncio>=0.21.0
46+
pytest-mock>=3.10.0
47+
black>=23.1.0
48+
isort>=5.12.0
49+
mypy>=1.0.0
50+
51+
# Security and certificate validation
52+
certifi>=2022.12.7
53+
54+
# Protocol buffer support (if needed for some ADK features)
55+
protobuf>=4.21.0
56+
57+
# JWT token handling (for some auth scenarios)
58+
PyJWT>=2.6.0
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Description
2+
This readme explains how to use [mcp inspector](https://modelcontextprotocol.io/docs/tools/inspector) to connect to OCI hosted remote MCP server. Once you have an ACTIVE Model deployment resource hosting your MCP server, we can use MCP inspector to connect and make list/call tools to MCP server.
3+
4+
# Prerequisites
5+
6+
To enable generating OCI Auth credentials before making API calls to Model deployment endpoints, we will create a local proxy server, which will intercept calls from inspector, created OCI auth and adds required header for outgoing calls.
7+
8+
- Install pip dependencies mentioned in [requirements.txt](./requirements.txt)
9+
```
10+
pip install -r requirements.txt
11+
```
12+
13+
- Start [oci_proxy.py](./oci_proxy.py)
14+
```
15+
python3 oci_proxy.py
16+
```
17+
18+
- Create OCI session and populate your local OCI config files with valid [session details](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/clitoken.htm)
19+
```
20+
oci session authenticate
21+
```
22+
23+
- Start mcp inspector on your local
24+
```
25+
npx @modelcontextprotocol/inspector
26+
```
27+
28+
- Configure MCP inspector with configuration as:
29+
- Transport type: Streamble HTTP
30+
- URL: http://localhost:8000/proxy/{endpoint-URI-Path}/predict?target_host={endpoint-region-host}
31+
32+
(Use model deployment invoke endpoint details from console to fill these values. For example : endpoint-URI-Path = ocid1.datasciencemodeldeployment.oc1....... and endpoint-region-host = modeldeployment.us-ashburn-1.oci.customer-oci.com)
33+
34+
- Click connect and the status should show "Connected", as shown in [screenshot](./screenshot.png)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import hashlib
2+
import base64
3+
from datetime import datetime
4+
from typing import Dict
5+
from urllib.parse import urlparse, parse_qs
6+
7+
import httpx
8+
from fastapi import FastAPI, Request, HTTPException, Depends
9+
from fastapi.responses import JSONResponse
10+
import oci
11+
import requests
12+
13+
app = FastAPI(title="OCI Authentication Proxy", version="1.0.0")
14+
15+
class OCIAuthProxy:
16+
def __init__(self, config_profile: str = "DEFAULT"):
17+
"""Initialize OCI configuration and signer"""
18+
try:
19+
# Load OCI config
20+
PROFILE_NAME = config_profile
21+
SECURITY_TOKEN_FILE_KEY = 'security_token_file'
22+
KEY_FILE_KEY = 'key_file'
23+
self.config = oci.config.from_file(profile_name=PROFILE_NAME)
24+
token_file = self.config[SECURITY_TOKEN_FILE_KEY]
25+
token = None
26+
with open(token_file, 'r') as f:
27+
token = f.read()
28+
private_key = oci.signer.load_private_key_from_file(self.config[KEY_FILE_KEY])
29+
self.signer = oci.auth.signers.SecurityTokenSigner(token, private_key, body_headers=["content-type", "x-content-sha256"])
30+
except Exception as e:
31+
raise Exception(f"Failed to initialize OCI config: {str(e)}")
32+
33+
def _get_content_hash(self, body: bytes) -> str:
34+
"""Generate SHA256 hash of request body"""
35+
return base64.b64encode(hashlib.sha256(body).digest()).decode('utf-8')
36+
37+
def _prepare_headers(self, method: str, url: str, headers: Dict[str, str], body: bytes) -> Dict[str, str]:
38+
"""Prepare headers for OCI authentication"""
39+
parsed_url = urlparse(url)
40+
41+
# Required headers for OCI
42+
auth_headers = {
43+
'date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
44+
'host': parsed_url.netloc,
45+
'(request-target)': f"{method.lower()} {parsed_url.path}"
46+
}
47+
48+
# Add query parameters to request-target if present
49+
if parsed_url.query:
50+
auth_headers['(request-target)'] += f"?{parsed_url.query}"
51+
52+
# Add content headers for POST/PUT/PATCH requests
53+
if method.upper() in ['POST', 'PUT', 'PATCH'] and body:
54+
auth_headers['content-type'] = headers.get('content-type', 'application/json')
55+
auth_headers['content-length'] = str(len(body))
56+
auth_headers['x-content-sha256'] = self._get_content_hash(body)
57+
58+
return auth_headers
59+
60+
async def make_authenticated_request(
61+
self,
62+
method: str,
63+
target_url: str,
64+
headers: Dict[str, str],
65+
body: bytes = b''
66+
) -> httpx.Response:
67+
"""Make an authenticated request to OCI API"""
68+
69+
request = requests.Request("POST", target_url, auth=self.signer, headers={'Content-Type': 'application/json', 'accept': 'application/json, text/event-stream'})
70+
prepared = request.prepare()
71+
del(prepared.headers['content-length'])
72+
73+
# Prepare final headers
74+
final_headers = prepared.headers
75+
76+
# Make the request
77+
async with httpx.AsyncClient(timeout=30.0) as client:
78+
response = await client.request(
79+
method=method,
80+
url=target_url,
81+
headers=final_headers,
82+
content=body
83+
)
84+
return response
85+
86+
# Global proxy instance
87+
proxy = None
88+
89+
async def get_proxy() -> OCIAuthProxy:
90+
"""Dependency to get or create proxy instance"""
91+
global proxy
92+
if proxy is None:
93+
try:
94+
proxy = OCIAuthProxy()
95+
except Exception as e:
96+
raise HTTPException(status_code=500, detail=f"Failed to initialize OCI proxy: {str(e)}")
97+
return proxy
98+
99+
@app.on_event("startup")
100+
async def startup_event():
101+
"""Initialize the proxy on startup"""
102+
await get_proxy()
103+
104+
@app.get("/health")
105+
async def health_check():
106+
"""Health check endpoint"""
107+
return {"status": "healthy", "service": "OCI Authentication Proxy"}
108+
109+
@app.api_route("/proxy/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
110+
async def proxy_request(
111+
request: Request,
112+
path: str,
113+
target_host: str,
114+
proxy_instance: OCIAuthProxy = Depends(get_proxy)
115+
):
116+
"""
117+
Proxy requests to OCI with authentication
118+
119+
Query Parameters:
120+
- target_host: The OCI service hostname (e.g., iaas.us-ashburn-1.oraclecloud.com)
121+
122+
Example:
123+
GET /proxy/20160918/instances?target_host=iaas.us-ashburn-1.oraclecloud.com
124+
"""
125+
126+
if not target_host:
127+
raise HTTPException(
128+
status_code=400,
129+
detail="target_host query parameter is required"
130+
)
131+
132+
# Construct target URL
133+
scheme = "https" # OCI APIs are always HTTPS
134+
query_string = str(request.url.query)
135+
136+
# Remove target_host from query parameters for the actual request
137+
if query_string:
138+
query_params = parse_qs(query_string)
139+
if 'target_host' in query_params:
140+
del query_params['target_host']
141+
# Reconstruct query string
142+
query_parts = []
143+
for key, values in query_params.items():
144+
for value in values:
145+
query_parts.append(f"{key}={value}")
146+
query_string = "&".join(query_parts)
147+
148+
target_url = f"{scheme}://{target_host}/{path}"
149+
if query_string:
150+
target_url += f"?{query_string}"
151+
152+
# Get request body
153+
body = await request.body()
154+
155+
# Get headers (excluding host and other proxy-specific headers)
156+
headers = dict(request.headers)
157+
158+
try:
159+
# Make authenticated request
160+
response = await proxy_instance.make_authenticated_request(
161+
method=request.method,
162+
target_url=target_url,
163+
headers=headers,
164+
body=body
165+
)
166+
print(response)
167+
168+
# Return response
169+
return JSONResponse(
170+
content=response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text,
171+
status_code=response.status_code,
172+
headers=dict(response.headers)
173+
)
174+
175+
except httpx.TimeoutException:
176+
raise HTTPException(status_code=504, detail="Gateway timeout")
177+
except httpx.RequestError as e:
178+
raise HTTPException(status_code=502, detail=f"Request failed: {str(e)}")
179+
except Exception as e:
180+
print(e)
181+
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
182+
183+
@app.get("/")
184+
async def root():
185+
"""Root endpoint with usage instructions"""
186+
return {
187+
"message": "OCI Authentication Proxy",
188+
"usage": {
189+
"endpoint": "/proxy/{path}",
190+
"required_param": "target_host",
191+
"example": "/proxy/20160918/instances?target_host=iaas.us-ashburn-1.oraclecloud.com"
192+
}
193+
}
194+
195+
if __name__ == "__main__":
196+
import uvicorn
197+
uvicorn.run(app, host="0.0.0.0", port=8000)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fastapi
2+
uvicorn
3+
httpx
4+
oci
5+
python-multipart
6+
requests
314 KB
Loading

0 commit comments

Comments
 (0)