eulerian-marketing-platform 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +1 -1
- eulerian_marketing_platform/server.py +158 -167
- {eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/METADATA +1 -2
- eulerian_marketing_platform-0.2.0.dist-info/RECORD +8 -0
- eulerian_marketing_platform-0.1.2.dist-info/RECORD +0 -8
- {eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/LICENSE +0 -0
- {eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/WHEEL +0 -0
- {eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/entry_points.txt +0 -0
- {eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Eulerian Marketing Platform MCP Proxy Server.
|
|
3
3
|
|
|
4
|
-
This server acts as a proxy
|
|
4
|
+
This server acts as a transparent proxy between local MCP clients (Claude Desktop, etc.)
|
|
5
5
|
and a remote Eulerian Marketing Platform MCP server via HTTP.
|
|
6
6
|
|
|
7
|
-
It
|
|
8
|
-
and
|
|
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
9
|
"""
|
|
10
10
|
|
|
11
11
|
import os
|
|
@@ -14,27 +14,19 @@ import json
|
|
|
14
14
|
import logging
|
|
15
15
|
import tempfile
|
|
16
16
|
from datetime import datetime
|
|
17
|
-
from typing import Any
|
|
18
|
-
from pathlib import Path
|
|
19
17
|
import httpx
|
|
20
18
|
|
|
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
19
|
# Configuration
|
|
27
20
|
EMP_API_ENDPOINT = os.environ.get("EMP_API_ENDPOINT")
|
|
28
21
|
EMP_API_TOKEN = os.environ.get("EMP_API_TOKEN")
|
|
29
22
|
|
|
30
23
|
# Logging setup - Use cross-platform temp directory
|
|
31
|
-
# Default to system temp directory if EMP_LOG_FILE not set
|
|
32
24
|
DEFAULT_LOG_FILE = os.path.join(tempfile.gettempdir(), "eulerian-mcp-proxy.log")
|
|
33
25
|
LOG_FILE = os.environ.get("EMP_LOG_FILE", DEFAULT_LOG_FILE)
|
|
34
26
|
|
|
35
27
|
# Ensure log directory exists
|
|
36
28
|
log_dir = os.path.dirname(LOG_FILE)
|
|
37
|
-
if log_dir:
|
|
29
|
+
if log_dir:
|
|
38
30
|
os.makedirs(log_dir, exist_ok=True)
|
|
39
31
|
|
|
40
32
|
# Configure logging to file and stderr with UTF-8 encoding for cross-platform compatibility
|
|
@@ -75,176 +67,175 @@ def validate_config() -> None:
|
|
|
75
67
|
sys.exit(1)
|
|
76
68
|
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
"""
|
|
70
|
+
def forward_request(request_data: dict) -> dict:
|
|
71
|
+
"""Forward a JSON-RPC request to the remote MCP server.
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.endpoint = EMP_API_ENDPOINT
|
|
84
|
-
self.token = EMP_API_TOKEN
|
|
85
|
-
self.timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
|
|
73
|
+
Args:
|
|
74
|
+
request_data: The JSON-RPC request to forward
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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")
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
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}")
|
|
94
89
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"id": 1,
|
|
108
|
-
"method": method,
|
|
109
|
-
}
|
|
110
|
-
if params:
|
|
111
|
-
request_data["params"] = params
|
|
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}")
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
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
115
|
|
|
116
|
+
# Parse response
|
|
116
117
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if response.status_code != 200:
|
|
132
|
-
error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
|
|
133
|
-
logger.error(f" Error: {response.text[:200]}")
|
|
134
|
-
raise Exception(error_msg)
|
|
135
|
-
|
|
136
|
-
response_data = response.json()
|
|
137
|
-
logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
|
|
138
|
-
|
|
139
|
-
# Validate JSON-RPC response
|
|
140
|
-
if "jsonrpc" not in response_data:
|
|
141
|
-
logger.warning(" WARNING: Missing 'jsonrpc' field")
|
|
142
|
-
|
|
143
|
-
if "result" in response_data:
|
|
144
|
-
logger.info(" Has 'result' field [OK]")
|
|
145
|
-
return response_data["result"]
|
|
146
|
-
elif "error" in response_data:
|
|
147
|
-
logger.error(f" Has 'error' field: {response_data['error']}")
|
|
148
|
-
raise Exception(f"Remote error: {response_data['error']}")
|
|
149
|
-
else:
|
|
150
|
-
logger.warning(" No 'result' or 'error' field")
|
|
151
|
-
return response_data
|
|
152
|
-
|
|
153
|
-
except httpx.TimeoutException:
|
|
154
|
-
logger.error("ERROR: Request timeout")
|
|
155
|
-
raise Exception("Request timeout")
|
|
156
|
-
except httpx.RequestError as e:
|
|
157
|
-
logger.error(f"ERROR: Request failed - {str(e)}")
|
|
158
|
-
raise Exception(f"Request failed: {str(e)}")
|
|
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
|
+
|
|
159
132
|
except json.JSONDecodeError as e:
|
|
160
|
-
logger.error(f"ERROR: Invalid JSON
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"""Entry point for the MCP proxy server."""
|
|
170
|
-
# Validate configuration before starting
|
|
171
|
-
validate_config()
|
|
172
|
-
|
|
173
|
-
# Create proxy and server instances (moved here to avoid running on import)
|
|
174
|
-
global proxy, mcp
|
|
175
|
-
proxy = EulerianMCPProxy()
|
|
176
|
-
mcp = FastMCP("eulerian-marketing-platform")
|
|
177
|
-
|
|
178
|
-
# Register tools
|
|
179
|
-
@mcp.tool()
|
|
180
|
-
async def list_remote_tools() -> dict[str, Any]:
|
|
181
|
-
"""List all available tools from the remote Eulerian MCP server."""
|
|
182
|
-
try:
|
|
183
|
-
result = await proxy.forward_request("tools/list")
|
|
184
|
-
return result
|
|
185
|
-
except Exception as e:
|
|
186
|
-
logger.error(f"Failed to list tools: {str(e)}")
|
|
187
|
-
return {"error": str(e), "tools": []}
|
|
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
|
+
}
|
|
188
142
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
logger.error(f"Failed to call tool '{tool_name}': {str(e)}")
|
|
200
|
-
return {"error": str(e), "tool": tool_name}
|
|
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
|
+
}
|
|
201
153
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|
|
211
164
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
222
181
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"protocolVersion": "2024-11-05",
|
|
229
|
-
"capabilities": {},
|
|
230
|
-
"clientInfo": {"name": "eulerian-mcp-proxy", "version": "0.1.1"}
|
|
231
|
-
})
|
|
232
|
-
return result
|
|
233
|
-
except Exception as e:
|
|
234
|
-
logger.error(f"Failed to get server info: {str(e)}")
|
|
235
|
-
return {"error": str(e)}
|
|
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()
|
|
236
187
|
|
|
237
|
-
logger.info("
|
|
238
|
-
logger.info("
|
|
239
|
-
logger.info("
|
|
240
|
-
logger.info("
|
|
241
|
-
logger.info("
|
|
242
|
-
logger.info(" - read_eulerian_resource: Read a specific resource")
|
|
243
|
-
logger.info(" - get_server_info: Get remote server information")
|
|
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")
|
|
244
193
|
|
|
245
|
-
# Run the server in stdio mode (default for MCP)
|
|
246
194
|
try:
|
|
247
|
-
|
|
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
|
+
|
|
248
239
|
except KeyboardInterrupt:
|
|
249
240
|
logger.info("Server stopped by user")
|
|
250
241
|
except Exception as e:
|
{eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/METADATA
RENAMED
|
@@ -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
|
|
@@ -24,7 +24,6 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: httpx (>=0.25.0)
|
|
27
|
-
Requires-Dist: mcp (>=1.2.0)
|
|
28
27
|
Provides-Extra: deployment
|
|
29
28
|
Requires-Dist: uv (>=0.5.0) ; extra == 'deployment'
|
|
30
29
|
Provides-Extra: dev
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
eulerian_marketing_platform/__init__.py,sha256=8Od_2veelx75AfbIEFrCS1UF6AOsKaX6CF4zR_z8yYU,428
|
|
2
|
+
eulerian_marketing_platform/server.py,sha256=y1vy-LvSjG18dvDv65hO875s_7vogbb5bdNwTetKKcU,8335
|
|
3
|
+
eulerian_marketing_platform-0.2.0.dist-info/LICENSE,sha256=eIqBqE_fRsqQJ8F-2v0e-8WzZqdshsCqnzmqLAWrNHU,1078
|
|
4
|
+
eulerian_marketing_platform-0.2.0.dist-info/METADATA,sha256=B0l6BxIaS-m55cFeicvSsHEhwVKZAmwy2OaFM9eWYpY,16928
|
|
5
|
+
eulerian_marketing_platform-0.2.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
6
|
+
eulerian_marketing_platform-0.2.0.dist-info/entry_points.txt,sha256=rrPZptATSS9PUtH9gzCYq0WuP6eahkF-DkdUP1FaYfk,88
|
|
7
|
+
eulerian_marketing_platform-0.2.0.dist-info/top_level.txt,sha256=nidh3T6fw-mLjUqZwQ8AiMScS4usuH0WXW4ZgG4HYCo,28
|
|
8
|
+
eulerian_marketing_platform-0.2.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
eulerian_marketing_platform/__init__.py,sha256=osuJ8iP6O20ElZXPoZlOQ7sNbZ93kgMz3we8mm_B2-Y,428
|
|
2
|
-
eulerian_marketing_platform/server.py,sha256=bUVn_b05ZritTtNflldCxoFn8YwyrQBqrcbQZ5ceZAg,9810
|
|
3
|
-
eulerian_marketing_platform-0.1.2.dist-info/LICENSE,sha256=eIqBqE_fRsqQJ8F-2v0e-8WzZqdshsCqnzmqLAWrNHU,1078
|
|
4
|
-
eulerian_marketing_platform-0.1.2.dist-info/METADATA,sha256=gfE3v09r1B4dd8Vi_snjgd9JQDWaHplKnZMsp1ML-34,16957
|
|
5
|
-
eulerian_marketing_platform-0.1.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
6
|
-
eulerian_marketing_platform-0.1.2.dist-info/entry_points.txt,sha256=rrPZptATSS9PUtH9gzCYq0WuP6eahkF-DkdUP1FaYfk,88
|
|
7
|
-
eulerian_marketing_platform-0.1.2.dist-info/top_level.txt,sha256=nidh3T6fw-mLjUqZwQ8AiMScS4usuH0WXW4ZgG4HYCo,28
|
|
8
|
-
eulerian_marketing_platform-0.1.2.dist-info/RECORD,,
|
{eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{eulerian_marketing_platform-0.1.2.dist-info → eulerian_marketing_platform-0.2.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|