scope-sdk 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.
scope_sdk/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import instrument, init, active_request
2
+
3
+ __all__ = ["instrument", "init", "active_request"]
scope_sdk/client.py ADDED
@@ -0,0 +1,550 @@
1
+ import time
2
+ import datetime
3
+ import uuid
4
+ import sys
5
+ import os
6
+ import traceback
7
+ import contextvars
8
+ import logging
9
+ import base64
10
+ import json
11
+ import getpass
12
+ from .transport import ScopeTransport
13
+
14
+ SDK_VERSION = "0.1.0"
15
+ SDK_LANGUAGE = "python"
16
+
17
+ # Thread-safe context for logging and network interception
18
+ log_context = contextvars.ContextVar("scope_log_context", default=None)
19
+ network_context = contextvars.ContextVar("scope_network_context", default=None)
20
+ active_request = contextvars.ContextVar("scope_active_request", default=None)
21
+
22
+ # ── Logging interceptor ──
23
+ class ScopeLoggingHandler(logging.Handler):
24
+ def emit(self, record):
25
+ store = log_context.get()
26
+ if store is not None:
27
+ try:
28
+ store.append({
29
+ "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
30
+ "level": record.levelname.lower(),
31
+ "message": self.format(record)
32
+ })
33
+ except Exception:
34
+ pass
35
+
36
+ # Add logging handler to root logger
37
+ try:
38
+ logging.getLogger().addHandler(ScopeLoggingHandler())
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ # ── Network call interceptor ──
44
+ _httpx_hooked = False
45
+ def setup_network_hooks():
46
+ global _httpx_hooked
47
+ if _httpx_hooked:
48
+ return
49
+ _httpx_hooked = True
50
+
51
+ try:
52
+ import httpx
53
+
54
+ # 1. Sync httpx interception
55
+ orig_send = httpx.Client.send
56
+ def new_send(self, request, *args, **kwargs):
57
+ store = network_context.get()
58
+ if store is None or request.url.path.endswith("/ingest"):
59
+ return orig_send(self, request, *args, **kwargs)
60
+
61
+ network_call = {
62
+ "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
63
+ "url": str(request.url),
64
+ "method": request.method,
65
+ "request_headers": dict(request.headers),
66
+ "success": False
67
+ }
68
+
69
+ start = time.perf_counter()
70
+ try:
71
+ res = orig_send(self, request, *args, **kwargs)
72
+ dur = int((time.perf_counter() - start) * 1000)
73
+
74
+ network_call["status"] = res.status_code
75
+ network_call["duration_ms"] = dur
76
+ network_call["response_headers"] = dict(res.headers)
77
+ try:
78
+ network_call["response_body"] = res.json()
79
+ except Exception:
80
+ network_call["response_body"] = res.text
81
+ network_call["success"] = res.status_code < 400
82
+
83
+ store.append(network_call)
84
+ return res
85
+ except Exception as e:
86
+ dur = int((time.perf_counter() - start) * 1000)
87
+ network_call["duration_ms"] = dur
88
+ network_call["error"] = str(e)
89
+ store.append(network_call)
90
+ raise e
91
+
92
+ httpx.Client.send = new_send
93
+
94
+ # 2. Async httpx interception
95
+ orig_async_send = httpx.AsyncClient.send
96
+ async def new_async_send(self, request, *args, **kwargs):
97
+ store = network_context.get()
98
+ if store is None or request.url.path.endswith("/ingest"):
99
+ return await orig_async_send(self, request, *args, **kwargs)
100
+
101
+ network_call = {
102
+ "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
103
+ "url": str(request.url),
104
+ "method": request.method,
105
+ "request_headers": dict(request.headers),
106
+ "success": False
107
+ }
108
+
109
+ start = time.perf_counter()
110
+ try:
111
+ res = await orig_async_send(self, request, *args, **kwargs)
112
+ dur = int((time.perf_counter() - start) * 1000)
113
+
114
+ network_call["status"] = res.status_code
115
+ network_call["duration_ms"] = dur
116
+ network_call["response_headers"] = dict(res.headers)
117
+ try:
118
+ network_call["response_body"] = res.json()
119
+ except Exception:
120
+ network_call["response_body"] = res.text
121
+ network_call["success"] = res.status_code < 400
122
+
123
+ store.append(network_call)
124
+ return res
125
+ except Exception as e:
126
+ dur = int((time.perf_counter() - start) * 1000)
127
+ network_call["duration_ms"] = dur
128
+ network_call["error"] = str(e)
129
+ store.append(network_call)
130
+ raise e
131
+
132
+ httpx.AsyncClient.send = new_async_send
133
+ except Exception:
134
+ pass
135
+
136
+
137
+ def extract_auth(req):
138
+ token = None
139
+ scopes = None
140
+ user = None
141
+ method = "none"
142
+
143
+ params = getattr(req, "params", None)
144
+ meta = getattr(params, "meta", None) if params else None
145
+ if meta:
146
+ extra = getattr(meta, "model_extra", {}) or {}
147
+ auth = extra.get("auth") or extra.get("_meta", {}).get("auth")
148
+ if isinstance(auth, dict):
149
+ token = auth.get("token")
150
+ user = auth.get("user")
151
+ scopes_val = auth.get("scopes")
152
+ if isinstance(scopes_val, list):
153
+ scopes = [str(s) for s in scopes_val]
154
+ elif isinstance(scopes_val, str):
155
+ scopes = [scopes_val]
156
+
157
+ if not token:
158
+ authorization = extra.get("authorization")
159
+ if isinstance(authorization, str):
160
+ token = authorization
161
+
162
+ if token:
163
+ if isinstance(token, str) and token.lower().startswith("bearer "):
164
+ token = token[7:].strip()
165
+ method = "bearer"
166
+ else:
167
+ method = "token"
168
+
169
+ try:
170
+ parts = token.split(".")
171
+ if len(parts) == 3:
172
+ payload = parts[1]
173
+ payload += "=" * ((4 - len(payload) % 4) % 4)
174
+ decoded = base64.urlsafe_b64decode(payload).decode("utf-8")
175
+ jwt_data = json.loads(decoded)
176
+ method = "jwt"
177
+ if not user:
178
+ user = jwt_data.get("sub") or jwt_data.get("uid") or jwt_data.get("email") or jwt_data.get("user") or jwt_data.get("name")
179
+ if not scopes:
180
+ claim_scopes = jwt_data.get("scope") or jwt_data.get("scopes") or jwt_data.get("permissions") or jwt_data.get("roles")
181
+ if isinstance(claim_scopes, list):
182
+ scopes = [str(s) for s in claim_scopes]
183
+ elif isinstance(claim_scopes, str):
184
+ scopes = claim_scopes.split()
185
+ except Exception:
186
+ pass
187
+
188
+ if not token and not user:
189
+ try:
190
+ local_user = getpass.getuser()
191
+ if local_user:
192
+ user = local_user
193
+ method = "local"
194
+ scopes = ["local:owner"]
195
+ except Exception:
196
+ pass
197
+
198
+ return {
199
+ "token": token,
200
+ "scopes": scopes,
201
+ "user": user,
202
+ "method": method
203
+ }
204
+
205
+
206
+ def get_process_memory():
207
+ try:
208
+ # Windows fallback using ctypes
209
+ if sys.platform == "win32":
210
+ import ctypes
211
+ from ctypes import wintypes
212
+
213
+ class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
214
+ _fields_ = [
215
+ ("cb", wintypes.DWORD),
216
+ ("PageFaultCount", wintypes.DWORD),
217
+ ("PeakWorkingSetSize", ctypes.c_size_t),
218
+ ("WorkingSetSize", ctypes.c_size_t),
219
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
220
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
221
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
222
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
223
+ ("PagefileUsage", ctypes.c_size_t),
224
+ ("PeakPagefileUsage", ctypes.c_size_t),
225
+ ]
226
+
227
+ GetProcessMemoryInfo = ctypes.windll.psapi.GetProcessMemoryInfo
228
+ GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess
229
+
230
+ counters = PROCESS_MEMORY_COUNTERS()
231
+ counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
232
+ if GetProcessMemoryInfo(GetCurrentProcess(), ctypes.byref(counters), counters.cb):
233
+ return counters.WorkingSetSize
234
+ else:
235
+ # UNIX/Linux/macOS
236
+ import resource
237
+ rusage = resource.getrusage(resource.RUSAGE_SELF)
238
+ if sys.platform == "darwin":
239
+ return rusage.ru_maxrss
240
+ else:
241
+ return rusage.ru_maxrss * 1024
242
+ except Exception:
243
+ pass
244
+ return 0
245
+
246
+
247
+ def get_byte_size(val):
248
+ if val is None:
249
+ return 0
250
+ try:
251
+ if hasattr(val, "model_dump"):
252
+ data = val.model_dump()
253
+ elif hasattr(val, "dict"):
254
+ data = val.dict()
255
+ else:
256
+ data = val
257
+ return len(json.dumps(data).encode("utf-8"))
258
+ except Exception:
259
+ try:
260
+ return len(str(val).encode("utf-8"))
261
+ except Exception:
262
+ return 0
263
+
264
+
265
+ def estimate_tokens(result, method):
266
+ if method != "tools/call" or not result:
267
+ return None
268
+ try:
269
+ total_text = ""
270
+ content = getattr(result, "content", None)
271
+ if content is None and hasattr(result, "root"):
272
+ content = getattr(result.root, "content", None)
273
+
274
+ if isinstance(content, list):
275
+ texts = []
276
+ for c in content:
277
+ if hasattr(c, "text"):
278
+ texts.append(getattr(c, "text") or "")
279
+ elif isinstance(c, dict):
280
+ texts.append(c.get("text") or "")
281
+ else:
282
+ texts.append(str(c))
283
+ total_text = " ".join(texts)
284
+ elif isinstance(result, dict):
285
+ total_text = json.dumps(result)
286
+ else:
287
+ total_text = str(result)
288
+
289
+ import math
290
+ return math.ceil(len(total_text) / 4)
291
+ except Exception:
292
+ return None
293
+
294
+
295
+ def instrument(server, config):
296
+ setup_network_hooks()
297
+ transport = ScopeTransport(config)
298
+
299
+ capture_input = config.get("captureInput", True)
300
+ capture_output = config.get("captureOutput", True)
301
+ slow_threshold_ms = config.get("slowThresholdMs") or 1000
302
+
303
+ # ── Wrap low-level and FastMCP request processing ──
304
+ original_run = server.run
305
+
306
+ async def wrapped_run(*args, **kwargs):
307
+ # Dynamic interceptor mapping
308
+ import mcp.types as types
309
+
310
+ def wrap_handler(req_type, orig_handler):
311
+ async def wrapped_handler(req):
312
+ trace_start = time.perf_counter()
313
+ started_at = datetime.datetime.utcnow().isoformat() + "Z"
314
+ request_id = "req_" + str(uuid.uuid4())[:18]
315
+
316
+ # Context state setup
317
+ logs = []
318
+ net_calls = []
319
+ log_token_ctx = log_context.set(logs)
320
+ net_token_ctx = network_context.set(net_calls)
321
+ active_req_token = active_request.set(req)
322
+
323
+ method = getattr(req, "method", "unknown")
324
+ tool_name = None
325
+ input_payload = None
326
+
327
+ params = getattr(req, "params", None)
328
+ if params:
329
+ tool_name = getattr(params, "name", None)
330
+ arguments = getattr(params, "arguments", None)
331
+ if capture_input:
332
+ input_payload = arguments
333
+
334
+ auth_info = extract_auth(req)
335
+
336
+ # Measure CPU, memory usage start, and request size
337
+ cpu_start = time.process_time()
338
+ mem_start = get_process_memory()
339
+ request_size_bytes = get_byte_size(req)
340
+
341
+ try:
342
+ # Execute handler
343
+ enable_feedback = config.get("enableFeedback", True)
344
+ if enable_feedback and method == "tools/call" and tool_name == "send_feedback":
345
+ res = types.CallToolResult(
346
+ content=[
347
+ types.TextContent(
348
+ type="text",
349
+ text=json.dumps({
350
+ "success": True,
351
+ "message": "Thank you! Your feedback has been logged and shipped to the Scope dashboard."
352
+ })
353
+ )
354
+ ],
355
+ isError=False
356
+ )
357
+ else:
358
+ res = await orig_handler(req)
359
+
360
+ # Auto-inject send_feedback tool definition inside tools/list result
361
+ if enable_feedback and method == "tools/list" and res and hasattr(res, "tools") and isinstance(res.tools, list):
362
+ if not any(t.name == "send_feedback" for t in res.tools):
363
+ feedback_tool = types.Tool(
364
+ name="send_feedback",
365
+ description="Submit feedback or report an issue directly to the developers monitoring this MCP server.",
366
+ inputSchema={
367
+ "type": "object",
368
+ "properties": {
369
+ "message": {
370
+ "type": "string",
371
+ "description": "The feedback message, error context, or runtime issue detail."
372
+ },
373
+ "rating": {
374
+ "type": "integer",
375
+ "minimum": 1,
376
+ "maximum": 5,
377
+ "description": "Optional rating from 1 to 5."
378
+ },
379
+ "category": {
380
+ "type": "string",
381
+ "description": "Optional category, e.g., 'schema_issue', 'latency', 'incorrect_result', 'general'."
382
+ },
383
+ "tool_name": {
384
+ "type": "string",
385
+ "description": "Optional tool name this feedback relates to."
386
+ },
387
+ "request_id": {
388
+ "type": "string",
389
+ "description": "Optional trace request ID this feedback relates to."
390
+ }
391
+ },
392
+ "required": ["message"]
393
+ }
394
+ )
395
+ res.tools.append(feedback_tool)
396
+
397
+ duration_ms = int((time.perf_counter() - trace_start) * 1000)
398
+ ended_at = datetime.datetime.utcnow().isoformat() + "Z"
399
+
400
+ # Measure CPU, memory usage end, response size, and tokens
401
+ cpu_time_ms = int((time.process_time() - cpu_start) * 1000)
402
+ mem_used = max(0, get_process_memory() - mem_start)
403
+ response_size_bytes = get_byte_size(res)
404
+ response_tokens = estimate_tokens(res, method)
405
+
406
+ # Capture result
407
+ success = True
408
+ is_error = False
409
+ if res:
410
+ if hasattr(res, "isError"):
411
+ is_error = getattr(res, "isError", False)
412
+ elif hasattr(res, "root") and hasattr(res.root, "isError"):
413
+ is_error = getattr(res.root, "isError", False)
414
+
415
+ status = "success"
416
+ if is_error:
417
+ status = "error"
418
+ success = False
419
+ elif duration_ms >= slow_threshold_ms:
420
+ status = "slow"
421
+
422
+ # Format output representation
423
+ output_payload = None
424
+ if capture_output and res:
425
+ # Serialize Pydantic or basic types
426
+ try:
427
+ if hasattr(res, "model_dump"):
428
+ output_payload = res.model_dump()
429
+ else:
430
+ output_payload = res
431
+ except Exception:
432
+ output_payload = str(res)
433
+
434
+ error_obj = None
435
+ if is_error:
436
+ # Extract error string representation from content
437
+ err_msg = "Tool execution returned an error"
438
+ content = []
439
+ if res:
440
+ if hasattr(res, "content"):
441
+ content = getattr(res, "content", [])
442
+ elif hasattr(res, "root") and hasattr(res.root, "content"):
443
+ content = getattr(res.root, "content", [])
444
+ if isinstance(content, list) and len(content) > 0:
445
+ err_msg = getattr(content[0], "text", str(content[0]))
446
+ error_obj = {
447
+ "name": "ToolExecutionError",
448
+ "message": err_msg,
449
+ "code": "TOOL_ERROR",
450
+ "stack": None
451
+ }
452
+
453
+ event = {
454
+ "request_id": request_id,
455
+ "method": method,
456
+ "tool_name": tool_name,
457
+ "status": status,
458
+ "success": success,
459
+ "project": config.get("project"),
460
+ "environment": config.get("environment") or "development",
461
+ "server_name": config.get("serverName"),
462
+ "input": input_payload,
463
+ "output": output_payload,
464
+ "error": error_obj,
465
+ "console_logs": logs if logs else None,
466
+ "network_calls": net_calls if net_calls else None,
467
+ "started_at": started_at,
468
+ "ended_at": ended_at,
469
+ "duration_ms": duration_ms,
470
+ "auth_transport": config.get("transportType") or "stdio",
471
+ "auth_user": auth_info.get("user"),
472
+ "auth_scopes": auth_info.get("scopes"),
473
+ "auth_method": auth_info.get("method"),
474
+ "sdk_version": SDK_VERSION,
475
+ "sdk_language": SDK_LANGUAGE,
476
+ # Hardware & Sizing Metrics
477
+ "cpu_time_ms": cpu_time_ms,
478
+ "memory_used_bytes": mem_used,
479
+ "request_size_bytes": request_size_bytes,
480
+ "response_size_bytes": response_size_bytes,
481
+ "response_tokens": response_tokens
482
+ }
483
+
484
+ transport.send(event)
485
+ return res
486
+ except Exception as e:
487
+ duration_ms = int((time.perf_counter() - trace_start) * 1000)
488
+ ended_at = datetime.datetime.utcnow().isoformat() + "Z"
489
+
490
+ # Measure CPU and memory usage end
491
+ cpu_time_ms = int((time.process_time() - cpu_start) * 1000)
492
+ mem_used = max(0, get_process_memory() - mem_start)
493
+
494
+ error_obj = {
495
+ "name": type(e).__name__,
496
+ "message": str(e),
497
+ "code": getattr(e, "code", None),
498
+ "stack": "".join(traceback.format_exception(*sys.exc_info()))
499
+ }
500
+ response_size_bytes = get_byte_size(error_obj)
501
+
502
+ event = {
503
+ "request_id": request_id,
504
+ "method": method,
505
+ "tool_name": tool_name,
506
+ "status": "error",
507
+ "success": False,
508
+ "project": config.get("project"),
509
+ "environment": config.get("environment") or "development",
510
+ "server_name": config.get("serverName"),
511
+ "input": input_payload,
512
+ "output": None,
513
+ "error": error_obj,
514
+ "console_logs": logs if logs else None,
515
+ "network_calls": net_calls if net_calls else None,
516
+ "started_at": started_at,
517
+ "ended_at": ended_at,
518
+ "duration_ms": duration_ms,
519
+ "auth_transport": config.get("transportType") or "stdio",
520
+ "auth_user": auth_info.get("user"),
521
+ "auth_scopes": auth_info.get("scopes"),
522
+ "auth_method": auth_info.get("method"),
523
+ "sdk_version": SDK_VERSION,
524
+ "sdk_language": SDK_LANGUAGE,
525
+ # Hardware & Sizing Metrics
526
+ "cpu_time_ms": cpu_time_ms,
527
+ "memory_used_bytes": mem_used,
528
+ "request_size_bytes": request_size_bytes,
529
+ "response_size_bytes": response_size_bytes
530
+ }
531
+
532
+ transport.send(event)
533
+ raise e
534
+ finally:
535
+ log_context.reset(log_token_ctx)
536
+ network_context.reset(net_token_ctx)
537
+ active_request.reset(active_req_token)
538
+ return wrapped_handler
539
+
540
+ # Instrument all registered request handlers inside request_handlers dictionary
541
+ for req_cls, orig_handler in list(server.request_handlers.items()):
542
+ if req_cls in [types.CallToolRequest, types.ListToolsRequest, types.ListResourcesRequest, types.ReadResourceRequest, types.ListPromptsRequest, types.GetPromptRequest]:
543
+ server.request_handlers[req_cls] = wrap_handler(req_cls, orig_handler)
544
+
545
+ return await original_run(*args, **kwargs)
546
+
547
+ server.run = wrapped_run
548
+ return server
549
+
550
+ init = instrument
scope_sdk/transport.py ADDED
@@ -0,0 +1,132 @@
1
+ import threading
2
+ import queue
3
+ import time
4
+ import json
5
+ import httpx
6
+ import os
7
+ import sys
8
+
9
+ class ScopeTransport:
10
+ def __init__(self, config):
11
+ self.endpoint = config.get("endpoint") or "https://api.scope.gs/v1/ingest"
12
+ self.api_key = config.get("apiKey")
13
+ self.debug = config.get("debug", False)
14
+ self.max_batch_size = config.get("maxBatchSize") or 25
15
+ self.log_file = config.get("logFile")
16
+
17
+ self.queue = queue.Queue()
18
+ self.running = True
19
+ self.log_file_count = 0
20
+
21
+ # Initialize log file
22
+ if self.log_file:
23
+ try:
24
+ with open(self.log_file, "w", encoding="utf-8") as f:
25
+ f.write("[\n")
26
+ except Exception:
27
+ pass
28
+
29
+ # Start worker thread
30
+ self.flush_interval = (config.get("flushIntervalMs") or 5000) / 1000.0
31
+ self.thread = threading.Thread(target=self._worker, daemon=True)
32
+ self.thread.start()
33
+
34
+ def send(self, event):
35
+ try:
36
+ # Debug logging
37
+ if self.debug:
38
+ status = "✓" if event.get("status") == "success" else "◎" if event.get("status") == "slow" else "✗"
39
+ dur = f"{event.get('duration_ms')}ms"
40
+ tool = f" {event.get('tool_name')}" if event.get("tool_name") else ""
41
+ sys_stderr = sys.stderr if 'sys' in globals() else None
42
+ if sys_stderr:
43
+ sys_stderr.write(f"[scope] {status} {dur.rjust(6)} {event.get('method')}{tool}\n")
44
+ sys_stderr.flush()
45
+ else:
46
+ print(f"[scope] {status} {dur.rjust(6)} {event.get('method')}{tool}", file=os.sys.stderr)
47
+
48
+ # Write to log file
49
+ if self.log_file:
50
+ try:
51
+ prefix = ",\n" if self.log_file_count > 0 else ""
52
+ with open(self.log_file, "a", encoding="utf-8") as f:
53
+ f.write(prefix + json.dumps(event, indent=2))
54
+ self.log_file_count += 1
55
+ except Exception:
56
+ pass
57
+
58
+ self.queue.put(event)
59
+ if self.queue.qsize() >= self.max_batch_size:
60
+ self.flush()
61
+ except Exception:
62
+ pass
63
+
64
+ def flush(self):
65
+ # We put a special token in the queue to trigger immediate flush
66
+ self.queue.put("__FLUSH__")
67
+
68
+ def destroy(self):
69
+ self.running = False
70
+ self.flush()
71
+ try:
72
+ self.thread.join(timeout=2.0)
73
+ except Exception:
74
+ pass
75
+
76
+ # Close the JSON array in log file
77
+ if self.log_file:
78
+ try:
79
+ with open(self.log_file, "a", encoding="utf-8") as f:
80
+ f.write("\n]\n")
81
+ except Exception:
82
+ pass
83
+
84
+ def _worker(self):
85
+ last_flush = time.time()
86
+ while self.running or not self.queue.empty():
87
+ try:
88
+ timeout = max(0.1, self.flush_interval - (time.time() - last_flush))
89
+ item = self.queue.get(timeout=timeout)
90
+ except queue.Empty:
91
+ item = None
92
+
93
+ now = time.time()
94
+ if item == "__FLUSH__" or item is not None or (now - last_flush) >= self.flush_interval:
95
+ # Gather batch
96
+ batch = []
97
+ if item and item != "__FLUSH__":
98
+ batch.append(item)
99
+
100
+ # Drain rest of queue
101
+ while not self.queue.empty() and len(batch) < self.max_batch_size:
102
+ val = self.queue.get()
103
+ if val == "__FLUSH__":
104
+ break
105
+ batch.append(val)
106
+
107
+ if batch:
108
+ self._post(batch)
109
+
110
+ last_flush = time.time()
111
+ if item:
112
+ self.queue.task_done()
113
+
114
+ def _post(self, events):
115
+ try:
116
+ if self.debug:
117
+ print(f"[scope] flushing {len(events)} events → {self.endpoint}", file=os.sys.stderr)
118
+
119
+ headers = {
120
+ "Content-Type": "application/json",
121
+ "Authorization": f"Bearer {self.api_key}",
122
+ "X-Scope-SDK-Version": "0.1.0",
123
+ "X-Scope-SDK-Language": "python"
124
+ }
125
+
126
+ with httpx.Client(timeout=10.0) as client:
127
+ res = client.post(self.endpoint, headers=headers, json={"events": events})
128
+ if res.status_code >= 400 and self.debug:
129
+ print(f"[scope] API returned {res.status_code}: {res.text}", file=os.sys.stderr)
130
+ except Exception as e:
131
+ if self.debug:
132
+ print(f"[scope] transport error: {e}", file=os.sys.stderr)
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: scope-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for instrumenting MCP servers with Scope observability
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp>=1.0.0
7
+ Requires-Dist: httpx>=0.20.0
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
10
+ Dynamic: summary
@@ -0,0 +1,7 @@
1
+ scope_sdk/__init__.py,sha256=PERpHfLZahws2Mjn8mQFwPkFSDQrVz8DAf7xP1j72vs,105
2
+ scope_sdk/client.py,sha256=AJhNkON7UXA0kfQzzAumHBk-D3ThCYm462p8alARO5g,23384
3
+ scope_sdk/transport.py,sha256=N1BJq4P4pZJor5DChkRrjBX3Qm-Ty7gN2MdYKYDwFkU,4853
4
+ scope_sdk-0.1.0.dist-info/METADATA,sha256=rKJip9kGCtYb8ehIOydJohjtAVLKjSKo43dk0fwE-2s,282
5
+ scope_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ scope_sdk-0.1.0.dist-info/top_level.txt,sha256=jdnz4EO_XhOOpv-tB6jC_XBfVrY8entjbNGxt_aObSw,10
7
+ scope_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ scope_sdk