marketing-data-platform 0.1.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.
- data_platform_agent_tools/__init__.py +5 -0
- data_platform_agent_tools/config.py +87 -0
- data_platform_agent_tools/login_cli.py +295 -0
- data_platform_agent_tools/mcp_server.py +74 -0
- marketing_data_platform-0.1.0.dist-info/METADATA +94 -0
- marketing_data_platform-0.1.0.dist-info/RECORD +8 -0
- marketing_data_platform-0.1.0.dist-info/WHEEL +4 -0
- marketing_data_platform-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, field_validator
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
DEFAULT_SESSION_FILE = "~/.config/data-platform/mcp.env"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Settings(BaseSettings):
|
|
11
|
+
model_config = SettingsConfigDict(extra="ignore")
|
|
12
|
+
|
|
13
|
+
api_base_url: str = Field(default="http://127.0.0.1:8002", alias="DATA_PLATFORM_API_BASE_URL")
|
|
14
|
+
api_session_token: str = Field(default="", alias="DATA_PLATFORM_API_SESSION_TOKEN")
|
|
15
|
+
session_file: str = Field(default=DEFAULT_SESSION_FILE, alias="DATA_PLATFORM_MCP_SESSION_FILE")
|
|
16
|
+
default_limit: int = Field(default=100, alias="DATA_PLATFORM_MCP_DEFAULT_LIMIT")
|
|
17
|
+
max_limit: int = Field(default=1000, alias="DATA_PLATFORM_MCP_MAX_LIMIT")
|
|
18
|
+
|
|
19
|
+
@field_validator("api_base_url", "session_file")
|
|
20
|
+
@classmethod
|
|
21
|
+
def non_empty_string(cls, value: str) -> str:
|
|
22
|
+
value = value.strip()
|
|
23
|
+
if not value:
|
|
24
|
+
raise ValueError("value must not be empty")
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
@field_validator("default_limit", "max_limit")
|
|
28
|
+
@classmethod
|
|
29
|
+
def positive_int(cls, value: int) -> int:
|
|
30
|
+
if value < 1:
|
|
31
|
+
raise ValueError("value must be positive")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache
|
|
36
|
+
def get_settings() -> Settings:
|
|
37
|
+
settings = Settings()
|
|
38
|
+
file_values = read_env_file(Path(settings.session_file).expanduser())
|
|
39
|
+
fallback_file = read_env_file(Path(DEFAULT_SESSION_FILE).expanduser())
|
|
40
|
+
|
|
41
|
+
return Settings(
|
|
42
|
+
DATA_PLATFORM_API_BASE_URL=first_value(
|
|
43
|
+
file_values.get("DATA_PLATFORM_API_BASE_URL"),
|
|
44
|
+
fallback_file.get("DATA_PLATFORM_API_BASE_URL"),
|
|
45
|
+
settings.api_base_url,
|
|
46
|
+
),
|
|
47
|
+
DATA_PLATFORM_API_SESSION_TOKEN=first_value(
|
|
48
|
+
file_values.get("DATA_PLATFORM_API_SESSION_TOKEN"),
|
|
49
|
+
fallback_file.get("DATA_PLATFORM_API_SESSION_TOKEN"),
|
|
50
|
+
settings.api_session_token,
|
|
51
|
+
),
|
|
52
|
+
DATA_PLATFORM_MCP_SESSION_FILE=settings.session_file,
|
|
53
|
+
DATA_PLATFORM_MCP_DEFAULT_LIMIT=int(
|
|
54
|
+
first_value(
|
|
55
|
+
file_values.get("DATA_PLATFORM_MCP_DEFAULT_LIMIT"),
|
|
56
|
+
fallback_file.get("DATA_PLATFORM_MCP_DEFAULT_LIMIT"),
|
|
57
|
+
str(settings.default_limit),
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
DATA_PLATFORM_MCP_MAX_LIMIT=int(
|
|
61
|
+
first_value(
|
|
62
|
+
file_values.get("DATA_PLATFORM_MCP_MAX_LIMIT"),
|
|
63
|
+
fallback_file.get("DATA_PLATFORM_MCP_MAX_LIMIT"),
|
|
64
|
+
str(settings.max_limit),
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def first_value(*values: str | None) -> str:
|
|
71
|
+
for value in values:
|
|
72
|
+
if value:
|
|
73
|
+
return value
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_env_file(path: Path) -> dict[str, str]:
|
|
78
|
+
if not path.exists():
|
|
79
|
+
return {}
|
|
80
|
+
values: dict[str, str] = {}
|
|
81
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
82
|
+
stripped = line.strip()
|
|
83
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
84
|
+
continue
|
|
85
|
+
key, value = stripped.split("=", 1)
|
|
86
|
+
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
87
|
+
return values
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import secrets
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import webbrowser
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
14
|
+
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
15
|
+
DEFAULT_SCOPES = "openid email profile"
|
|
16
|
+
DEFAULT_MCP_ENV_PATH = "~/.config/data-platform/mcp.env"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OAuthCallbackState:
|
|
20
|
+
def __init__(self, expected_state: str) -> None:
|
|
21
|
+
self.expected_state = expected_state
|
|
22
|
+
self.code: str | None = None
|
|
23
|
+
self.error: str | None = None
|
|
24
|
+
self.event = threading.Event()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
|
|
30
|
+
client_id = args.client_id or read_env_value("DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_ID")
|
|
31
|
+
client_secret = args.client_secret or read_env_value("DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_SECRET")
|
|
32
|
+
if not client_id:
|
|
33
|
+
raise SystemExit("DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_ID or --client-id is required.")
|
|
34
|
+
if not client_secret:
|
|
35
|
+
raise SystemExit("DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_SECRET or --client-secret is required.")
|
|
36
|
+
|
|
37
|
+
redirect_uri = f"http://127.0.0.1:{args.port}/callback"
|
|
38
|
+
state = secrets.token_urlsafe(24)
|
|
39
|
+
callback_state = OAuthCallbackState(expected_state=state)
|
|
40
|
+
|
|
41
|
+
server = build_callback_server(args.port, callback_state)
|
|
42
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
43
|
+
server_thread.start()
|
|
44
|
+
|
|
45
|
+
auth_url = build_auth_url(
|
|
46
|
+
client_id=client_id,
|
|
47
|
+
redirect_uri=redirect_uri,
|
|
48
|
+
state=state,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print("Opening browser for Google login...")
|
|
52
|
+
if not webbrowser.open(auth_url):
|
|
53
|
+
print(f"Open this URL manually:\n{auth_url}")
|
|
54
|
+
|
|
55
|
+
if not callback_state.event.wait(timeout=args.timeout_seconds):
|
|
56
|
+
server.shutdown()
|
|
57
|
+
raise SystemExit("Timed out waiting for Google OAuth callback.")
|
|
58
|
+
server.shutdown()
|
|
59
|
+
|
|
60
|
+
if callback_state.error:
|
|
61
|
+
raise SystemExit(f"Google OAuth failed: {callback_state.error}")
|
|
62
|
+
if not callback_state.code:
|
|
63
|
+
raise SystemExit("Google OAuth callback did not include an authorization code.")
|
|
64
|
+
|
|
65
|
+
id_token = exchange_code_for_id_token(
|
|
66
|
+
code=callback_state.code,
|
|
67
|
+
client_id=client_id,
|
|
68
|
+
client_secret=client_secret,
|
|
69
|
+
redirect_uri=redirect_uri,
|
|
70
|
+
)
|
|
71
|
+
session = exchange_id_token_for_session(
|
|
72
|
+
api_base_url=args.api_base_url,
|
|
73
|
+
id_token=id_token,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
write_mcp_env(
|
|
77
|
+
output_path=args.output,
|
|
78
|
+
api_base_url=args.api_base_url,
|
|
79
|
+
session_token=session["access_token"],
|
|
80
|
+
default_limit=args.default_limit,
|
|
81
|
+
max_limit=args.max_limit,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
print(f"Logged in as {session.get('email')} ({session.get('role')})")
|
|
85
|
+
print(f"Saved MCP session config to {args.output}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_args() -> argparse.Namespace:
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
description="Login with Google and save a Data Platform API session for MCP."
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--api-base-url",
|
|
94
|
+
default=read_env_value("DATA_PLATFORM_API_BASE_URL") or "http://127.0.0.1:8002",
|
|
95
|
+
help="Data Platform API base URL.",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--client-id",
|
|
99
|
+
default=None,
|
|
100
|
+
help="Google OAuth client id. Defaults to DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_ID.",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--client-secret",
|
|
104
|
+
default=None,
|
|
105
|
+
help="Google OAuth client secret. Defaults to DATA_PLATFORM_GOOGLE_OAUTH_CLIENT_SECRET.",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--output",
|
|
109
|
+
default=DEFAULT_MCP_ENV_PATH,
|
|
110
|
+
help="Path to write MCP environment config.",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument("--port", type=int, default=8765, help="Local OAuth callback port.")
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--timeout-seconds",
|
|
115
|
+
type=int,
|
|
116
|
+
default=180,
|
|
117
|
+
help="Seconds to wait for OAuth callback.",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument("--default-limit", type=int, default=100)
|
|
120
|
+
parser.add_argument("--max-limit", type=int, default=1000)
|
|
121
|
+
return parser.parse_args()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def build_callback_server(port: int, callback_state: OAuthCallbackState) -> ThreadingHTTPServer:
|
|
125
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
126
|
+
def do_GET(self) -> None: # noqa: N802
|
|
127
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
128
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
129
|
+
received_state = first(params.get("state"))
|
|
130
|
+
if parsed.path != "/callback":
|
|
131
|
+
self.respond(404, "Not found.")
|
|
132
|
+
return
|
|
133
|
+
if received_state != callback_state.expected_state:
|
|
134
|
+
callback_state.error = "OAuth state mismatch."
|
|
135
|
+
callback_state.event.set()
|
|
136
|
+
self.respond(400, "Login failed: state mismatch.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
error = first(params.get("error"))
|
|
140
|
+
if error:
|
|
141
|
+
callback_state.error = error
|
|
142
|
+
callback_state.event.set()
|
|
143
|
+
self.respond(400, "Login failed. You can close this tab.")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
callback_state.code = first(params.get("code"))
|
|
147
|
+
callback_state.event.set()
|
|
148
|
+
self.respond(200, "Login complete. You can close this tab.")
|
|
149
|
+
|
|
150
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
def respond(self, status_code: int, body: str) -> None:
|
|
154
|
+
encoded = body.encode("utf-8")
|
|
155
|
+
self.send_response(status_code)
|
|
156
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
157
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
158
|
+
self.end_headers()
|
|
159
|
+
self.wfile.write(encoded)
|
|
160
|
+
|
|
161
|
+
return ThreadingHTTPServer(("127.0.0.1", port), CallbackHandler)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_auth_url(*, client_id: str, redirect_uri: str, state: str) -> str:
|
|
165
|
+
query = urllib.parse.urlencode(
|
|
166
|
+
{
|
|
167
|
+
"client_id": client_id,
|
|
168
|
+
"redirect_uri": redirect_uri,
|
|
169
|
+
"response_type": "code",
|
|
170
|
+
"scope": DEFAULT_SCOPES,
|
|
171
|
+
"state": state,
|
|
172
|
+
"access_type": "offline",
|
|
173
|
+
"prompt": "select_account",
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
return f"{AUTH_URL}?{query}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def exchange_code_for_id_token(
|
|
180
|
+
*,
|
|
181
|
+
code: str,
|
|
182
|
+
client_id: str,
|
|
183
|
+
client_secret: str,
|
|
184
|
+
redirect_uri: str,
|
|
185
|
+
) -> str:
|
|
186
|
+
response = httpx.post(
|
|
187
|
+
TOKEN_URL,
|
|
188
|
+
data={
|
|
189
|
+
"code": code,
|
|
190
|
+
"client_id": client_id,
|
|
191
|
+
"client_secret": client_secret,
|
|
192
|
+
"redirect_uri": redirect_uri,
|
|
193
|
+
"grant_type": "authorization_code",
|
|
194
|
+
},
|
|
195
|
+
timeout=30,
|
|
196
|
+
)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
payload = response.json()
|
|
199
|
+
id_token = payload.get("id_token")
|
|
200
|
+
if not id_token:
|
|
201
|
+
raise SystemExit("Google token response did not include id_token.")
|
|
202
|
+
return str(id_token)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def exchange_id_token_for_session(*, api_base_url: str, id_token: str) -> dict[str, Any]:
|
|
206
|
+
session_url = f"{api_base_url.rstrip('/')}/auth/google/session"
|
|
207
|
+
try:
|
|
208
|
+
response = httpx.post(
|
|
209
|
+
session_url,
|
|
210
|
+
json={"id_token": id_token},
|
|
211
|
+
timeout=30,
|
|
212
|
+
)
|
|
213
|
+
except httpx.ConnectError as exc:
|
|
214
|
+
raise SystemExit(
|
|
215
|
+
"Could not connect to Data Platform API. "
|
|
216
|
+
f"Start it first, or pass the correct --api-base-url. Tried: {session_url}"
|
|
217
|
+
) from exc
|
|
218
|
+
if response.status_code == 403:
|
|
219
|
+
raise SystemExit("Login succeeded, but this email is not allowlisted.")
|
|
220
|
+
response.raise_for_status()
|
|
221
|
+
payload = response.json()
|
|
222
|
+
if "access_token" not in payload:
|
|
223
|
+
raise SystemExit("Data Platform API response did not include access_token.")
|
|
224
|
+
return payload
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def write_mcp_env(
|
|
228
|
+
*,
|
|
229
|
+
output_path: str,
|
|
230
|
+
api_base_url: str,
|
|
231
|
+
session_token: str,
|
|
232
|
+
default_limit: int,
|
|
233
|
+
max_limit: int,
|
|
234
|
+
) -> None:
|
|
235
|
+
path = Path(output_path).expanduser()
|
|
236
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
path.write_text(
|
|
238
|
+
"\n".join(
|
|
239
|
+
[
|
|
240
|
+
f"DATA_PLATFORM_API_BASE_URL={api_base_url.rstrip('/')}",
|
|
241
|
+
f"DATA_PLATFORM_API_SESSION_TOKEN={session_token}",
|
|
242
|
+
f"DATA_PLATFORM_MCP_DEFAULT_LIMIT={default_limit}",
|
|
243
|
+
f"DATA_PLATFORM_MCP_MAX_LIMIT={max_limit}",
|
|
244
|
+
"",
|
|
245
|
+
]
|
|
246
|
+
),
|
|
247
|
+
encoding="utf-8",
|
|
248
|
+
)
|
|
249
|
+
path.chmod(0o600)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def read_env_value(name: str) -> str | None:
|
|
253
|
+
import os
|
|
254
|
+
|
|
255
|
+
value = os.getenv(name)
|
|
256
|
+
if value:
|
|
257
|
+
return value.strip()
|
|
258
|
+
|
|
259
|
+
for env_path in [
|
|
260
|
+
Path(".env"),
|
|
261
|
+
Path(".env.local"),
|
|
262
|
+
Path("apps/data-platform-api/.env"),
|
|
263
|
+
Path("apps/data-platform-api/.env.local"),
|
|
264
|
+
]:
|
|
265
|
+
value = read_env_file_value(env_path, name)
|
|
266
|
+
if value:
|
|
267
|
+
return value
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def read_env_file_value(path: Path, name: str) -> str | None:
|
|
272
|
+
if not path.exists():
|
|
273
|
+
return None
|
|
274
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
275
|
+
stripped = line.strip()
|
|
276
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
277
|
+
continue
|
|
278
|
+
key, value = stripped.split("=", 1)
|
|
279
|
+
if key.strip() == name:
|
|
280
|
+
return value.strip().strip('"').strip("'")
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def first(values: list[str] | None) -> str | None:
|
|
285
|
+
if not values:
|
|
286
|
+
return None
|
|
287
|
+
return values[0]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
try:
|
|
292
|
+
main()
|
|
293
|
+
except httpx.HTTPStatusError as exc:
|
|
294
|
+
print(f"HTTP error: {exc.response.status_code} {exc.response.text}", file=sys.stderr)
|
|
295
|
+
raise SystemExit(1) from exc
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
from data_platform_agent_tools.config import get_settings
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("data-platform-mart")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def bounded_limit(limit: int | None) -> int:
|
|
12
|
+
settings = get_settings()
|
|
13
|
+
value = limit or settings.default_limit
|
|
14
|
+
return min(max(value, 1), settings.max_limit)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@mcp.tool()
|
|
18
|
+
def get_daily_ad_performance(
|
|
19
|
+
start_date: str,
|
|
20
|
+
end_date: str,
|
|
21
|
+
client_name: str | None = None,
|
|
22
|
+
project_id: str | None = None,
|
|
23
|
+
media: str | None = None,
|
|
24
|
+
campaign_id: str | None = None,
|
|
25
|
+
external_campaign_id: str | None = None,
|
|
26
|
+
ad_group_id: str | None = None,
|
|
27
|
+
external_ad_group_id: str | None = None,
|
|
28
|
+
ad_id: str | None = None,
|
|
29
|
+
external_ad_id: str | None = None,
|
|
30
|
+
limit: int | None = None,
|
|
31
|
+
) -> list[dict[str, Any]]:
|
|
32
|
+
"""Read daily ad performance rows through the Data Platform API."""
|
|
33
|
+
settings = get_settings()
|
|
34
|
+
if not settings.api_session_token:
|
|
35
|
+
raise RuntimeError(
|
|
36
|
+
"MCP session token is missing. Run `data-platform-login` first, "
|
|
37
|
+
"or set DATA_PLATFORM_API_SESSION_TOKEN."
|
|
38
|
+
)
|
|
39
|
+
params: dict[str, str | int] = {
|
|
40
|
+
"start_date": start_date,
|
|
41
|
+
"end_date": end_date,
|
|
42
|
+
"limit": bounded_limit(limit),
|
|
43
|
+
}
|
|
44
|
+
for key, value in {
|
|
45
|
+
"client_name": client_name,
|
|
46
|
+
"project_id": project_id,
|
|
47
|
+
"media": media,
|
|
48
|
+
"campaign_id": campaign_id,
|
|
49
|
+
"external_campaign_id": external_campaign_id,
|
|
50
|
+
"ad_group_id": ad_group_id,
|
|
51
|
+
"external_ad_group_id": external_ad_group_id,
|
|
52
|
+
"ad_id": ad_id,
|
|
53
|
+
"external_ad_id": external_ad_id,
|
|
54
|
+
}.items():
|
|
55
|
+
if value is not None:
|
|
56
|
+
params[key] = value
|
|
57
|
+
|
|
58
|
+
response = httpx.get(
|
|
59
|
+
f"{settings.api_base_url.rstrip('/')}/agent/mart/daily-ad-performance",
|
|
60
|
+
params=params,
|
|
61
|
+
headers={"Authorization": f"Bearer {settings.api_session_token}"},
|
|
62
|
+
timeout=30,
|
|
63
|
+
)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
payload = response.json()
|
|
66
|
+
return payload["rows"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main() -> None:
|
|
70
|
+
mcp.run()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marketing-data-platform
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Login CLI and MCP server for the Data Platform API.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Requires-Dist: mcp>=1.2.0
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.6.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Marketing Data Platform
|
|
12
|
+
|
|
13
|
+
Python package for local agents and MCP clients that need to access the Data Platform API.
|
|
14
|
+
|
|
15
|
+
It provides two commands:
|
|
16
|
+
|
|
17
|
+
- `data-platform-login`: login with Google and save a local Data Platform API session.
|
|
18
|
+
- `data-platform-mcp`: start the MCP server that exposes mart access tools.
|
|
19
|
+
|
|
20
|
+
The MCP server does not connect to BigQuery directly. It calls the Data Platform API, and the API owns BigQuery credentials, authorization, and tenant access control.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
For local development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run --package marketing-data-platform data-platform-login --help
|
|
28
|
+
uv run --package marketing-data-platform data-platform-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After publishing to PyPI:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx --from marketing-data-platform data-platform-login --help
|
|
35
|
+
uvx --from marketing-data-platform data-platform-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Login
|
|
39
|
+
|
|
40
|
+
The Data Platform API must be running and configured with Google OAuth.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uvx --from marketing-data-platform data-platform-login \
|
|
44
|
+
--api-base-url https://data-platform-api.example.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
By default, the command writes the MCP session file to:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
~/.config/data-platform/mcp.env
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The session token is saved locally with file mode `600` and is not printed.
|
|
54
|
+
|
|
55
|
+
## MCP Config
|
|
56
|
+
|
|
57
|
+
For a published package:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[mcp_servers.data-platform-mart]
|
|
61
|
+
command = "uvx"
|
|
62
|
+
args = ["--from", "marketing-data-platform", "data-platform-mcp"]
|
|
63
|
+
startup_timeout_sec = 120
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For this monorepo during development:
|
|
67
|
+
|
|
68
|
+
```toml
|
|
69
|
+
[mcp_servers.data-platform-mart]
|
|
70
|
+
command = "uv"
|
|
71
|
+
args = [
|
|
72
|
+
"run",
|
|
73
|
+
"--directory",
|
|
74
|
+
"/path/to/data-platform",
|
|
75
|
+
"--package",
|
|
76
|
+
"marketing-data-platform",
|
|
77
|
+
"data-platform-mcp",
|
|
78
|
+
]
|
|
79
|
+
startup_timeout_sec = 120
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## MCP Tools
|
|
83
|
+
|
|
84
|
+
- `get_daily_ad_performance`
|
|
85
|
+
|
|
86
|
+
## Environment Variables
|
|
87
|
+
|
|
88
|
+
| Name | Required | Description |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `DATA_PLATFORM_API_BASE_URL` | optional | Data Platform API base URL. Default: `http://127.0.0.1:8002` |
|
|
91
|
+
| `DATA_PLATFORM_API_SESSION_TOKEN` | optional | Session JWT issued by login. Usually read from the session file. |
|
|
92
|
+
| `DATA_PLATFORM_MCP_SESSION_FILE` | optional | Session env file. Default: `~/.config/data-platform/mcp.env` |
|
|
93
|
+
| `DATA_PLATFORM_MCP_DEFAULT_LIMIT` | optional | Default row limit. Default: `100` |
|
|
94
|
+
| `DATA_PLATFORM_MCP_MAX_LIMIT` | optional | Max row limit. Default: `1000` |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
data_platform_agent_tools/__init__.py,sha256=bvvRHhIbw1cKJ6YOmLJq6fYN8HE2X9F5MIofFK1sgMw,101
|
|
2
|
+
data_platform_agent_tools/config.py,sha256=gYP5Sjpc2FpsYiYJj-hzVRFOCX1vo6-0XoIAU9HpVWU,3097
|
|
3
|
+
data_platform_agent_tools/login_cli.py,sha256=H8WJ1SyouA52MpmDVj_utFsOkRNqVwCApTq_wgrh3u4,9558
|
|
4
|
+
data_platform_agent_tools/mcp_server.py,sha256=SrnJasmV-Ba8ShFTDrHQ1N-A93eKcHaG987ipy9hYaI,2110
|
|
5
|
+
marketing_data_platform-0.1.0.dist-info/METADATA,sha256=0xi8qIVEEEVfOohWXhqKs-YZFo8uGMuWVPWUQzl8bu8,2558
|
|
6
|
+
marketing_data_platform-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
marketing_data_platform-0.1.0.dist-info/entry_points.txt,sha256=f2PmBpsGyLspVepGo2v9UD3UNFtmGJVqZZCCkjtxEMY,143
|
|
8
|
+
marketing_data_platform-0.1.0.dist-info/RECORD,,
|