eulerian-marketing-platform 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/PKG-INFO +1 -1
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/pyproject.toml +1 -9
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/setup.cfg +1 -1
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform/__init__.py +8 -4
- eulerian_marketing_platform-0.2.0/src/eulerian_marketing_platform/server.py +247 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/PKG-INFO +1 -1
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/requires.txt +0 -1
- eulerian_marketing_platform-0.1.1/src/eulerian_marketing_platform/server.py +0 -310
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/.env.example +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/LICENSE +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/MANIFEST.in +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/README.md +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/requirements.txt +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/SOURCES.txt +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/dependency_links.txt +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/entry_points.txt +0 -0
- {eulerian_marketing_platform-0.1.1 → eulerian_marketing_platform-0.2.0}/src/eulerian_marketing_platform.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: eulerian_marketing_platform
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: MCP server for Eulerian Marketing Platform - enables AI assistants to interact with Eulerian's marketing analytics and campaign management APIs
|
|
5
5
|
Author-email: Eulerian Technologies <mathieu@eulerian.com>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eulerian_marketing_platform"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "MCP server for Eulerian Marketing Platform - enables AI assistants to interact with Eulerian's marketing analytics and campaign management APIs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -26,7 +26,6 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"mcp>=1.2.0",
|
|
30
29
|
"httpx>=0.25.0",
|
|
31
30
|
]
|
|
32
31
|
requires-python = ">=3.10"
|
|
@@ -53,12 +52,5 @@ Issues = "https://github.com/EulerianTechnologies/eulerian-marketing-platform-mc
|
|
|
53
52
|
Documentation = "https://github.com/EulerianTechnologies/eulerian-marketing-platform-mcp#readme"
|
|
54
53
|
Repository = "https://github.com/EulerianTechnologies/eulerian-marketing-platform-mcp"
|
|
55
54
|
|
|
56
|
-
[tool.setuptools]
|
|
57
|
-
package-dir = {"" = "src"}
|
|
58
|
-
|
|
59
55
|
[tool.setuptools.packages.find]
|
|
60
56
|
where = ["src"]
|
|
61
|
-
namespaces = false
|
|
62
|
-
|
|
63
|
-
[tool.setuptools.package-data]
|
|
64
|
-
eulerian_marketing_platform = ["py.typed"]
|
|
@@ -4,8 +4,12 @@ This package provides a Model Context Protocol (MCP) server that enables
|
|
|
4
4
|
AI assistants to interact with Eulerian Marketing Platform APIs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
__version__ = "0.1.1"
|
|
7
|
+
__version__ = "0.2.0"
|
|
10
8
|
__author__ = "Eulerian Technologies"
|
|
11
|
-
__all__ = [
|
|
9
|
+
__all__ = []
|
|
10
|
+
|
|
11
|
+
# Import main only when explicitly requested
|
|
12
|
+
def get_main():
|
|
13
|
+
"""Lazy import of main function to avoid side effects."""
|
|
14
|
+
from .server import main
|
|
15
|
+
return main
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Eulerian Marketing Platform MCP Proxy Server.
|
|
3
|
+
|
|
4
|
+
This server acts as a transparent proxy between local MCP clients (Claude Desktop, etc.)
|
|
5
|
+
and a remote Eulerian Marketing Platform MCP server via HTTP.
|
|
6
|
+
|
|
7
|
+
It forwards ALL MCP requests directly to the remote server, making all remote tools
|
|
8
|
+
and resources available to the client without any intermediary logic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import tempfile
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
# Configuration
|
|
20
|
+
EMP_API_ENDPOINT = os.environ.get("EMP_API_ENDPOINT")
|
|
21
|
+
EMP_API_TOKEN = os.environ.get("EMP_API_TOKEN")
|
|
22
|
+
|
|
23
|
+
# Logging setup - Use cross-platform temp directory
|
|
24
|
+
DEFAULT_LOG_FILE = os.path.join(tempfile.gettempdir(), "eulerian-mcp-proxy.log")
|
|
25
|
+
LOG_FILE = os.environ.get("EMP_LOG_FILE", DEFAULT_LOG_FILE)
|
|
26
|
+
|
|
27
|
+
# Ensure log directory exists
|
|
28
|
+
log_dir = os.path.dirname(LOG_FILE)
|
|
29
|
+
if log_dir:
|
|
30
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
# Configure logging to file and stderr with UTF-8 encoding for cross-platform compatibility
|
|
33
|
+
file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8')
|
|
34
|
+
file_handler.setLevel(logging.INFO)
|
|
35
|
+
file_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
|
|
36
|
+
|
|
37
|
+
# For stderr, use system encoding with error handling
|
|
38
|
+
stream_handler = logging.StreamHandler(sys.stderr)
|
|
39
|
+
stream_handler.setLevel(logging.INFO)
|
|
40
|
+
stream_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
|
|
41
|
+
|
|
42
|
+
# Configure root logger
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
level=logging.INFO,
|
|
45
|
+
handlers=[file_handler, stream_handler]
|
|
46
|
+
)
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_config() -> None:
|
|
51
|
+
"""Validate that required environment variables are set."""
|
|
52
|
+
missing_vars = []
|
|
53
|
+
|
|
54
|
+
if not EMP_API_ENDPOINT:
|
|
55
|
+
missing_vars.append("EMP_API_ENDPOINT")
|
|
56
|
+
if not EMP_API_TOKEN:
|
|
57
|
+
missing_vars.append("EMP_API_TOKEN")
|
|
58
|
+
|
|
59
|
+
if missing_vars:
|
|
60
|
+
logger.error("The following required environment variables are missing:")
|
|
61
|
+
for var in missing_vars:
|
|
62
|
+
logger.error(f" - {var}")
|
|
63
|
+
logger.error("\nPlease set these environment variables before running the server.")
|
|
64
|
+
logger.error("Example:")
|
|
65
|
+
logger.error(" export EMP_API_ENDPOINT=https://your-eulerian-instance.com/api/mcp")
|
|
66
|
+
logger.error(" export EMP_API_TOKEN=your_authentication_token")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def forward_request(request_data: dict) -> dict:
|
|
71
|
+
"""Forward a JSON-RPC request to the remote MCP server.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
request_data: The JSON-RPC request to forward
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The JSON-RPC response from the remote server
|
|
78
|
+
"""
|
|
79
|
+
timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
|
|
80
|
+
|
|
81
|
+
request_id = request_data.get("id")
|
|
82
|
+
method = request_data.get("method")
|
|
83
|
+
|
|
84
|
+
logger.info(f">>> REQUEST: {method} (id: {request_id})")
|
|
85
|
+
logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
logger.info(f"Forwarding to {EMP_API_ENDPOINT}")
|
|
89
|
+
|
|
90
|
+
# Use httpx for sync HTTP request
|
|
91
|
+
with httpx.Client(timeout=timeout) as client:
|
|
92
|
+
response = client.post(
|
|
93
|
+
EMP_API_ENDPOINT,
|
|
94
|
+
headers={
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
"Authorization": f"Bearer {EMP_API_TOKEN}"
|
|
97
|
+
},
|
|
98
|
+
json=request_data
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
|
|
102
|
+
|
|
103
|
+
if response.status_code != 200:
|
|
104
|
+
error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
|
|
105
|
+
logger.error(f" Error: {response.text[:200]}")
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"jsonrpc": "2.0",
|
|
109
|
+
"id": request_id,
|
|
110
|
+
"error": {
|
|
111
|
+
"code": -32000,
|
|
112
|
+
"message": error_msg
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Parse response
|
|
117
|
+
try:
|
|
118
|
+
response_data = response.json()
|
|
119
|
+
logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
|
|
120
|
+
|
|
121
|
+
# Validate JSON-RPC response
|
|
122
|
+
if "jsonrpc" not in response_data:
|
|
123
|
+
logger.warning(" WARNING: Missing 'jsonrpc' field")
|
|
124
|
+
|
|
125
|
+
if "result" in response_data:
|
|
126
|
+
logger.info(" Has 'result' field [OK]")
|
|
127
|
+
elif "error" in response_data:
|
|
128
|
+
logger.info(f" Has 'error' field: {response_data['error']}")
|
|
129
|
+
|
|
130
|
+
return response_data
|
|
131
|
+
|
|
132
|
+
except json.JSONDecodeError as e:
|
|
133
|
+
logger.error(f" ERROR: Invalid JSON - {e}")
|
|
134
|
+
return {
|
|
135
|
+
"jsonrpc": "2.0",
|
|
136
|
+
"id": request_id,
|
|
137
|
+
"error": {
|
|
138
|
+
"code": -32700,
|
|
139
|
+
"message": f"Invalid JSON: {str(e)}"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
except httpx.TimeoutException:
|
|
144
|
+
logger.error("ERROR: Request timeout")
|
|
145
|
+
return {
|
|
146
|
+
"jsonrpc": "2.0",
|
|
147
|
+
"id": request_id,
|
|
148
|
+
"error": {
|
|
149
|
+
"code": -32000,
|
|
150
|
+
"message": "Request timeout"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
except httpx.RequestError as e:
|
|
155
|
+
logger.error(f"ERROR: Request failed - {str(e)}")
|
|
156
|
+
return {
|
|
157
|
+
"jsonrpc": "2.0",
|
|
158
|
+
"id": request_id,
|
|
159
|
+
"error": {
|
|
160
|
+
"code": -32000,
|
|
161
|
+
"message": f"Request failed: {str(e)}"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"ERROR: Unexpected error - {str(e)}")
|
|
167
|
+
import traceback
|
|
168
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
169
|
+
return {
|
|
170
|
+
"jsonrpc": "2.0",
|
|
171
|
+
"id": request_id,
|
|
172
|
+
"error": {
|
|
173
|
+
"code": -32000,
|
|
174
|
+
"message": f"Error: {str(e)}"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> None:
|
|
180
|
+
"""Entry point for the MCP proxy server.
|
|
181
|
+
|
|
182
|
+
Runs a transparent stdio proxy that forwards all JSON-RPC requests
|
|
183
|
+
to the remote Eulerian MCP server.
|
|
184
|
+
"""
|
|
185
|
+
# Validate configuration
|
|
186
|
+
validate_config()
|
|
187
|
+
|
|
188
|
+
logger.info("=== EULERIAN MCP PROXY START ===")
|
|
189
|
+
logger.info(f"Endpoint: {EMP_API_ENDPOINT}")
|
|
190
|
+
logger.info(f"Token: {EMP_API_TOKEN[:10] if EMP_API_TOKEN else 'None'}...")
|
|
191
|
+
logger.info(f"Timeout: {float(os.environ.get('EMP_TIMEOUT', '300'))}s")
|
|
192
|
+
logger.info("Starting stdio proxy - all remote tools will be available")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Read from stdin line by line
|
|
196
|
+
for line in sys.stdin:
|
|
197
|
+
line = line.strip()
|
|
198
|
+
if not line:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Parse request
|
|
203
|
+
request_data = json.loads(line)
|
|
204
|
+
|
|
205
|
+
# Forward to remote server
|
|
206
|
+
response_data = forward_request(request_data)
|
|
207
|
+
|
|
208
|
+
# Send response to stdout
|
|
209
|
+
response_json = json.dumps(response_data)
|
|
210
|
+
print(response_json, flush=True)
|
|
211
|
+
logger.info(" Response forwarded [OK]")
|
|
212
|
+
|
|
213
|
+
except json.JSONDecodeError as e:
|
|
214
|
+
logger.error(f"ERROR: Invalid JSON in request - {e}")
|
|
215
|
+
error_response = {
|
|
216
|
+
"jsonrpc": "2.0",
|
|
217
|
+
"id": None,
|
|
218
|
+
"error": {
|
|
219
|
+
"code": -32700,
|
|
220
|
+
"message": f"Parse error: {str(e)}"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
print(json.dumps(error_response), flush=True)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"ERROR: Unexpected error processing request - {str(e)}")
|
|
227
|
+
import traceback
|
|
228
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
229
|
+
error_response = {
|
|
230
|
+
"jsonrpc": "2.0",
|
|
231
|
+
"id": None,
|
|
232
|
+
"error": {
|
|
233
|
+
"code": -32000,
|
|
234
|
+
"message": f"Error: {str(e)}"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
print(json.dumps(error_response), flush=True)
|
|
238
|
+
|
|
239
|
+
except KeyboardInterrupt:
|
|
240
|
+
logger.info("Server stopped by user")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Server error: {str(e)}")
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: eulerian-marketing-platform
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: MCP server for Eulerian Marketing Platform - enables AI assistants to interact with Eulerian's marketing analytics and campaign management APIs
|
|
5
5
|
Author-email: Eulerian Technologies <mathieu@eulerian.com>
|
|
6
6
|
License: MIT
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Eulerian Marketing Platform MCP Proxy Server.
|
|
3
|
-
|
|
4
|
-
This server acts as a proxy/bridge between local MCP clients (Claude Desktop, Gemini CLI, etc.)
|
|
5
|
-
and a remote Eulerian Marketing Platform MCP server via HTTP.
|
|
6
|
-
|
|
7
|
-
It uses EMP_API_ENDPOINT and EMP_API_TOKEN environment variables to authenticate
|
|
8
|
-
and forward requests to the remote MCP server.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import os
|
|
12
|
-
import sys
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
import tempfile
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
from typing import Any
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
import httpx
|
|
20
|
-
|
|
21
|
-
from mcp.server.fastmcp import FastMCP
|
|
22
|
-
from mcp.server import Server
|
|
23
|
-
from mcp.server.stdio import stdio_server
|
|
24
|
-
from mcp import types
|
|
25
|
-
|
|
26
|
-
# Configuration
|
|
27
|
-
EMP_API_ENDPOINT = os.environ.get("EMP_API_ENDPOINT")
|
|
28
|
-
EMP_API_TOKEN = os.environ.get("EMP_API_TOKEN")
|
|
29
|
-
|
|
30
|
-
# Logging setup - Use cross-platform temp directory
|
|
31
|
-
# Default to system temp directory if EMP_LOG_FILE not set
|
|
32
|
-
DEFAULT_LOG_FILE = os.path.join(tempfile.gettempdir(), "eulerian-mcp-proxy.log")
|
|
33
|
-
LOG_FILE = os.environ.get("EMP_LOG_FILE", DEFAULT_LOG_FILE)
|
|
34
|
-
|
|
35
|
-
# Ensure log directory exists
|
|
36
|
-
log_dir = os.path.dirname(LOG_FILE)
|
|
37
|
-
if log_dir: # Only create if there's a directory part
|
|
38
|
-
os.makedirs(log_dir, exist_ok=True)
|
|
39
|
-
|
|
40
|
-
# Configure logging to file and stderr
|
|
41
|
-
logging.basicConfig(
|
|
42
|
-
level=logging.INFO,
|
|
43
|
-
format="[%(asctime)s] %(levelname)s: %(message)s",
|
|
44
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
45
|
-
handlers=[
|
|
46
|
-
logging.FileHandler(LOG_FILE),
|
|
47
|
-
logging.StreamHandler(sys.stderr)
|
|
48
|
-
]
|
|
49
|
-
)
|
|
50
|
-
logger = logging.getLogger(__name__)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def validate_config() -> None:
|
|
54
|
-
"""Validate that required environment variables are set."""
|
|
55
|
-
missing_vars = []
|
|
56
|
-
|
|
57
|
-
if not EMP_API_ENDPOINT:
|
|
58
|
-
missing_vars.append("EMP_API_ENDPOINT")
|
|
59
|
-
if not EMP_API_TOKEN:
|
|
60
|
-
missing_vars.append("EMP_API_TOKEN")
|
|
61
|
-
|
|
62
|
-
if missing_vars:
|
|
63
|
-
logger.error("The following required environment variables are missing:")
|
|
64
|
-
for var in missing_vars:
|
|
65
|
-
logger.error(f" - {var}")
|
|
66
|
-
logger.error("\nPlease set these environment variables before running the server.")
|
|
67
|
-
logger.error("Example:")
|
|
68
|
-
logger.error(" export EMP_API_ENDPOINT=https://your-eulerian-instance.com/api/mcp")
|
|
69
|
-
logger.error(" export EMP_API_TOKEN=your_authentication_token")
|
|
70
|
-
sys.exit(1)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class EulerianMCPProxy:
|
|
74
|
-
"""Proxy that forwards MCP requests to remote Eulerian server."""
|
|
75
|
-
|
|
76
|
-
def __init__(self):
|
|
77
|
-
"""Initialize the proxy with configuration."""
|
|
78
|
-
self.endpoint = EMP_API_ENDPOINT
|
|
79
|
-
self.token = EMP_API_TOKEN
|
|
80
|
-
self.timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
|
|
81
|
-
|
|
82
|
-
logger.info("=== EULERIAN MCP PROXY START ===")
|
|
83
|
-
logger.info(f"Endpoint: {self.endpoint}")
|
|
84
|
-
logger.info(f"Token: {self.token[:10] if self.token else 'None'}...")
|
|
85
|
-
logger.info(f"Timeout: {self.timeout}s")
|
|
86
|
-
|
|
87
|
-
async def forward_request(self, method: str, params: dict = None) -> dict[str, Any]:
|
|
88
|
-
"""Forward a JSON-RPC request to the remote MCP server.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
method: The JSON-RPC method name
|
|
92
|
-
params: The parameters for the method
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
The response from the remote server
|
|
96
|
-
|
|
97
|
-
Raises:
|
|
98
|
-
Exception: If the request fails
|
|
99
|
-
"""
|
|
100
|
-
request_data = {
|
|
101
|
-
"jsonrpc": "2.0",
|
|
102
|
-
"id": 1,
|
|
103
|
-
"method": method,
|
|
104
|
-
}
|
|
105
|
-
if params:
|
|
106
|
-
request_data["params"] = params
|
|
107
|
-
|
|
108
|
-
logger.info(f">>> REQUEST: {method}")
|
|
109
|
-
logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
async with httpx.AsyncClient() as client:
|
|
113
|
-
logger.info(f"Forwarding to {self.endpoint}")
|
|
114
|
-
response = await client.post(
|
|
115
|
-
self.endpoint,
|
|
116
|
-
headers={
|
|
117
|
-
"Content-Type": "application/json",
|
|
118
|
-
"Authorization": f"Bearer {self.token}"
|
|
119
|
-
},
|
|
120
|
-
json=request_data,
|
|
121
|
-
timeout=self.timeout
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
|
|
125
|
-
|
|
126
|
-
if response.status_code != 200:
|
|
127
|
-
error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
|
|
128
|
-
logger.error(f" Error: {response.text[:200]}")
|
|
129
|
-
raise Exception(error_msg)
|
|
130
|
-
|
|
131
|
-
response_data = response.json()
|
|
132
|
-
logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
|
|
133
|
-
|
|
134
|
-
# Validate JSON-RPC response
|
|
135
|
-
if "jsonrpc" not in response_data:
|
|
136
|
-
logger.warning(" WARNING: Missing 'jsonrpc' field")
|
|
137
|
-
|
|
138
|
-
if "result" in response_data:
|
|
139
|
-
logger.info(" Has 'result' field [OK]")
|
|
140
|
-
return response_data["result"]
|
|
141
|
-
elif "error" in response_data:
|
|
142
|
-
logger.error(f" Has 'error' field: {response_data['error']}")
|
|
143
|
-
raise Exception(f"Remote error: {response_data['error']}")
|
|
144
|
-
else:
|
|
145
|
-
logger.warning(" No 'result' or 'error' field")
|
|
146
|
-
return response_data
|
|
147
|
-
|
|
148
|
-
except httpx.TimeoutException:
|
|
149
|
-
logger.error("ERROR: Request timeout")
|
|
150
|
-
raise Exception("Request timeout")
|
|
151
|
-
except httpx.RequestError as e:
|
|
152
|
-
logger.error(f"ERROR: Request failed - {str(e)}")
|
|
153
|
-
raise Exception(f"Request failed: {str(e)}")
|
|
154
|
-
except json.JSONDecodeError as e:
|
|
155
|
-
logger.error(f"ERROR: Invalid JSON response - {str(e)}")
|
|
156
|
-
raise Exception(f"Invalid JSON response: {str(e)}")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# Create global proxy instance
|
|
160
|
-
proxy = EulerianMCPProxy()
|
|
161
|
-
|
|
162
|
-
# Create FastMCP server
|
|
163
|
-
mcp = FastMCP("eulerian-marketing-platform")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# Dynamically fetch and register tools from remote server
|
|
167
|
-
@mcp.tool()
|
|
168
|
-
async def list_remote_tools() -> dict[str, Any]:
|
|
169
|
-
"""List all available tools from the remote Eulerian MCP server.
|
|
170
|
-
|
|
171
|
-
This tool queries the remote MCP server to discover what tools are available.
|
|
172
|
-
Use this to see what operations you can perform on the Eulerian platform.
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
Dictionary containing the list of available tools with their descriptions
|
|
176
|
-
"""
|
|
177
|
-
try:
|
|
178
|
-
result = await proxy.forward_request("tools/list")
|
|
179
|
-
return result
|
|
180
|
-
except Exception as e:
|
|
181
|
-
logger.error(f"Failed to list tools: {str(e)}")
|
|
182
|
-
return {"error": str(e), "tools": []}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
@mcp.tool()
|
|
186
|
-
async def call_eulerian_tool(tool_name: str, arguments: dict[str, Any] = None) -> dict[str, Any]:
|
|
187
|
-
"""Call a tool on the remote Eulerian MCP server.
|
|
188
|
-
|
|
189
|
-
This is a generic tool that forwards requests to the remote Eulerian platform.
|
|
190
|
-
First use list_remote_tools() to see what tools are available, then call them
|
|
191
|
-
using this function.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
tool_name: The name of the tool to call on the remote server
|
|
195
|
-
arguments: Dictionary of arguments to pass to the tool (optional)
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
The result from the remote tool execution
|
|
199
|
-
|
|
200
|
-
Example:
|
|
201
|
-
To call a tool named "search_goal" with no arguments:
|
|
202
|
-
>>> call_eulerian_tool("search_goal")
|
|
203
|
-
|
|
204
|
-
To call a tool with arguments:
|
|
205
|
-
>>> call_eulerian_tool("update_goal", {"action-id": "12345","action-name":"test-mcp"})
|
|
206
|
-
"""
|
|
207
|
-
if arguments is None:
|
|
208
|
-
arguments = {}
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
params = {
|
|
212
|
-
"name": tool_name,
|
|
213
|
-
"arguments": arguments
|
|
214
|
-
}
|
|
215
|
-
result = await proxy.forward_request("tools/call", params)
|
|
216
|
-
return result
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.error(f"Failed to call tool '{tool_name}': {str(e)}")
|
|
219
|
-
return {"error": str(e), "tool": tool_name}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@mcp.tool()
|
|
223
|
-
async def get_eulerian_resources() -> dict[str, Any]:
|
|
224
|
-
"""List all available resources from the remote Eulerian MCP server.
|
|
225
|
-
|
|
226
|
-
Resources are data sources that can be read from the Eulerian platform,
|
|
227
|
-
such as configuration files, reports, or reference data.
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
Dictionary containing the list of available resources
|
|
231
|
-
"""
|
|
232
|
-
try:
|
|
233
|
-
result = await proxy.forward_request("resources/list")
|
|
234
|
-
return result
|
|
235
|
-
except Exception as e:
|
|
236
|
-
logger.error(f"Failed to list resources: {str(e)}")
|
|
237
|
-
return {"error": str(e), "resources": []}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
@mcp.tool()
|
|
241
|
-
async def read_eulerian_resource(uri: str) -> dict[str, Any]:
|
|
242
|
-
"""Read a specific resource from the remote Eulerian MCP server.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
uri: The URI of the resource to read (get from get_eulerian_resources())
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
The content of the requested resource
|
|
249
|
-
|
|
250
|
-
Example:
|
|
251
|
-
>>> read_eulerian_resource("eulerian://config/settings")
|
|
252
|
-
"""
|
|
253
|
-
try:
|
|
254
|
-
params = {"uri": uri}
|
|
255
|
-
result = await proxy.forward_request("resources/read", params)
|
|
256
|
-
return result
|
|
257
|
-
except Exception as e:
|
|
258
|
-
logger.error(f"Failed to read resource '{uri}': {str(e)}")
|
|
259
|
-
return {"error": str(e), "uri": uri}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
@mcp.tool()
|
|
263
|
-
async def get_server_info() -> dict[str, Any]:
|
|
264
|
-
"""Get information about the remote Eulerian MCP server.
|
|
265
|
-
|
|
266
|
-
Returns server capabilities, version, and other metadata.
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
Dictionary containing server information
|
|
270
|
-
"""
|
|
271
|
-
try:
|
|
272
|
-
result = await proxy.forward_request("initialize", {
|
|
273
|
-
"protocolVersion": "2024-11-05",
|
|
274
|
-
"capabilities": {},
|
|
275
|
-
"clientInfo": {
|
|
276
|
-
"name": "eulerian-mcp-proxy",
|
|
277
|
-
"version": "0.1.0"
|
|
278
|
-
}
|
|
279
|
-
})
|
|
280
|
-
return result
|
|
281
|
-
except Exception as e:
|
|
282
|
-
logger.error(f"Failed to get server info: {str(e)}")
|
|
283
|
-
return {"error": str(e)}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def main() -> None:
|
|
287
|
-
"""Entry point for the MCP proxy server."""
|
|
288
|
-
# Validate configuration before starting
|
|
289
|
-
validate_config()
|
|
290
|
-
|
|
291
|
-
logger.info("Starting Eulerian MCP Proxy Server...")
|
|
292
|
-
logger.info("Available tools:")
|
|
293
|
-
logger.info(" - list_remote_tools: List all tools from remote server")
|
|
294
|
-
logger.info(" - call_eulerian_tool: Call any remote tool")
|
|
295
|
-
logger.info(" - get_eulerian_resources: List available resources")
|
|
296
|
-
logger.info(" - read_eulerian_resource: Read a specific resource")
|
|
297
|
-
logger.info(" - get_server_info: Get remote server information")
|
|
298
|
-
|
|
299
|
-
# Run the server in stdio mode (default for MCP)
|
|
300
|
-
try:
|
|
301
|
-
mcp.run()
|
|
302
|
-
except KeyboardInterrupt:
|
|
303
|
-
logger.info("Server stopped by user")
|
|
304
|
-
except Exception as e:
|
|
305
|
-
logger.error(f"Server error: {str(e)}")
|
|
306
|
-
raise
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if __name__ == "__main__":
|
|
310
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|