asap-protocol 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.
@@ -0,0 +1,739 @@
1
+ """FastAPI server implementation for ASAP protocol.
2
+
3
+ This module provides a production-ready FastAPI server that:
4
+ - Exposes POST /asap endpoint for JSON-RPC 2.0 wrapped ASAP messages
5
+ - Exposes GET /.well-known/asap/manifest.json for agent discovery
6
+ - Exposes GET /asap/metrics for Prometheus-compatible metrics
7
+ - Handles errors with proper JSON-RPC error responses
8
+ - Validates all incoming requests against ASAP schemas
9
+ - Uses HandlerRegistry for extensible payload processing
10
+ - Provides structured logging for observability
11
+ - Supports authentication based on manifest configuration
12
+
13
+ Example:
14
+ >>> from asap.models.entities import Manifest, Capability, Endpoint, Skill, AuthScheme
15
+ >>> from asap.transport.server import create_app
16
+ >>> from asap.transport.handlers import HandlerRegistry
17
+ >>>
18
+ >>> manifest = Manifest(
19
+ ... id="urn:asap:agent:my-agent",
20
+ ... name="My Agent",
21
+ ... version="1.0.0",
22
+ ... description="Example agent",
23
+ ... capabilities=Capability(
24
+ ... asap_version="0.1",
25
+ ... skills=[Skill(id="echo", description="Echo skill")],
26
+ ... state_persistence=False
27
+ ... ),
28
+ ... endpoints=Endpoint(asap="http://localhost:8000/asap"),
29
+ ... auth=AuthScheme(schemes=["bearer"]) # Optional authentication
30
+ ... )
31
+ >>>
32
+ >>> # Create app with default registry
33
+ >>> app = create_app(manifest)
34
+ >>>
35
+ >>> # Or with custom registry and auth
36
+ >>> registry = HandlerRegistry()
37
+ >>> registry.register("task.request", my_custom_handler)
38
+ >>> app = create_app(manifest, registry)
39
+ >>>
40
+ >>> # Run with: uvicorn asap.transport.server:app --host 0.0.0.0 --port 8000
41
+ """
42
+
43
+ import time
44
+ from typing import Any, Callable
45
+
46
+ from fastapi import FastAPI, HTTPException, Request
47
+ from fastapi.responses import JSONResponse, PlainTextResponse
48
+ from pydantic import ValidationError
49
+
50
+ from asap.models.entities import Capability, Endpoint, Manifest, Skill
51
+ from asap.models.envelope import Envelope
52
+ from asap.observability import get_logger, get_metrics
53
+ from asap.transport.middleware import AuthenticationMiddleware, BearerTokenValidator
54
+ from asap.observability.metrics import MetricsCollector
55
+ from asap.transport.handlers import (
56
+ HandlerNotFoundError,
57
+ HandlerRegistry,
58
+ create_default_registry,
59
+ )
60
+ from asap.transport.jsonrpc import (
61
+ ASAP_METHOD,
62
+ INTERNAL_ERROR,
63
+ INVALID_PARAMS,
64
+ INVALID_REQUEST,
65
+ METHOD_NOT_FOUND,
66
+ PARSE_ERROR,
67
+ JsonRpcError,
68
+ JsonRpcErrorResponse,
69
+ JsonRpcRequest,
70
+ JsonRpcResponse,
71
+ )
72
+
73
+ # Module logger
74
+ logger = get_logger(__name__)
75
+
76
+
77
+ class ASAPRequestHandler:
78
+ """Handler for processing ASAP protocol requests.
79
+
80
+ Encapsulates the logic for:
81
+ - Parsing and validating JSON-RPC requests
82
+ - Authenticating requests based on manifest configuration
83
+ - Validating sender identity
84
+ - Dispatching to registered handlers
85
+ - Building error responses
86
+ - Recording metrics
87
+
88
+ This class is instantiated by create_app() and used to handle
89
+ incoming requests on the /asap endpoint.
90
+
91
+ Attributes:
92
+ registry: Handler registry for payload dispatch
93
+ manifest: Agent manifest for context
94
+ auth_middleware: Optional authentication middleware
95
+
96
+ Example:
97
+ >>> handler = ASAPRequestHandler(registry, manifest, auth_middleware)
98
+ >>> response = await handler.handle_message(request)
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ registry: HandlerRegistry,
104
+ manifest: Manifest,
105
+ auth_middleware: AuthenticationMiddleware | None = None,
106
+ ) -> None:
107
+ """Initialize the request handler.
108
+
109
+ Args:
110
+ registry: Handler registry for dispatching payloads
111
+ manifest: Agent manifest describing capabilities
112
+ auth_middleware: Optional authentication middleware for request validation
113
+ """
114
+ self.registry = registry
115
+ self.manifest = manifest
116
+ self.auth_middleware = auth_middleware
117
+
118
+ def build_error_response(
119
+ self,
120
+ code: int,
121
+ data: dict[str, Any] | None = None,
122
+ request_id: str | int | None = None,
123
+ ) -> JSONResponse:
124
+ """Build a JSON-RPC error response.
125
+
126
+ Args:
127
+ code: JSON-RPC error code
128
+ data: Optional error data
129
+ request_id: Optional request ID from original request
130
+
131
+ Returns:
132
+ JSONResponse with error
133
+ """
134
+ error_response = JsonRpcErrorResponse(
135
+ error=JsonRpcError.from_code(code, data=data),
136
+ id=request_id,
137
+ )
138
+ return JSONResponse(status_code=200, content=error_response.model_dump())
139
+
140
+ def record_error_metrics(
141
+ self,
142
+ metrics: MetricsCollector,
143
+ payload_type: str,
144
+ error_type: str,
145
+ duration_seconds: float,
146
+ ) -> None:
147
+ """Record error metrics for a failed request.
148
+
149
+ Args:
150
+ metrics: Metrics collector instance
151
+ payload_type: Payload type (or "unknown")
152
+ error_type: Type of error that occurred
153
+ duration_seconds: Request duration in seconds
154
+ """
155
+ metrics.increment_counter(
156
+ "asap_requests_total",
157
+ {"payload_type": payload_type, "status": "error"},
158
+ )
159
+ metrics.increment_counter(
160
+ "asap_requests_error_total",
161
+ {"payload_type": payload_type, "error_type": error_type},
162
+ )
163
+ metrics.observe_histogram(
164
+ "asap_request_duration_seconds",
165
+ duration_seconds,
166
+ {"payload_type": payload_type, "status": "error"},
167
+ )
168
+
169
+ async def parse_json_body(self, request: Request) -> dict[str, Any]:
170
+ """Parse JSON body from request.
171
+
172
+ Args:
173
+ request: FastAPI request object
174
+
175
+ Returns:
176
+ Parsed JSON body
177
+
178
+ Raises:
179
+ ValueError: If JSON is invalid
180
+ """
181
+ try:
182
+ body: dict[str, Any] = await request.json()
183
+ return body
184
+ except ValueError as e:
185
+ logger.warning("asap.request.invalid_json", error=str(e))
186
+ raise
187
+
188
+ def validate_jsonrpc_request(
189
+ self, body: dict[str, Any]
190
+ ) -> tuple[JsonRpcRequest | None, JSONResponse | None]:
191
+ """Validate JSON-RPC request structure and method.
192
+
193
+ Args:
194
+ body: Parsed JSON body
195
+
196
+ Returns:
197
+ Tuple of (JsonRpcRequest, None) if valid, or (None, error_response) if invalid
198
+ """
199
+ # Validate JSON-RPC structure
200
+ try:
201
+ rpc_request = JsonRpcRequest(**body)
202
+ except (ValidationError, TypeError) as e:
203
+ # Check if error is specifically about params type
204
+ error_code = INVALID_REQUEST
205
+ error_message = "Invalid JSON-RPC structure"
206
+ if isinstance(e, ValidationError):
207
+ errors = e.errors()
208
+ # If params validation failed with dict_type error, use INVALID_PARAMS
209
+ for error in errors:
210
+ if error.get("loc") == ("params",) and error.get("type") == "dict_type":
211
+ error_code = INVALID_PARAMS
212
+ error_message = "JSON-RPC 'params' must be an object"
213
+ break
214
+
215
+ logger.warning(
216
+ "asap.request.invalid_structure",
217
+ error=error_message,
218
+ error_type=type(e).__name__,
219
+ validation_errors=str(e.errors()) if isinstance(e, ValidationError) else str(e),
220
+ )
221
+ error_response = self.build_error_response(
222
+ error_code,
223
+ data={
224
+ "error": error_message,
225
+ "validation_errors": (
226
+ e.errors()
227
+ if isinstance(e, ValidationError)
228
+ else [{"type": "type_error", "loc": (), "msg": str(e), "input": None}]
229
+ ),
230
+ },
231
+ request_id=body.get("id") if isinstance(body, dict) else None,
232
+ )
233
+ return None, error_response
234
+
235
+ # Check method
236
+ if rpc_request.method != ASAP_METHOD:
237
+ logger.warning("asap.request.unknown_method", method=rpc_request.method)
238
+ error_response = self.build_error_response(
239
+ METHOD_NOT_FOUND,
240
+ data={"method": rpc_request.method},
241
+ request_id=rpc_request.id,
242
+ )
243
+ return None, error_response
244
+
245
+ return rpc_request, None
246
+
247
+ async def handle_message(self, request: Request) -> JSONResponse:
248
+ """Handle ASAP messages wrapped in JSON-RPC 2.0.
249
+
250
+ This method:
251
+ 1. Receives JSON-RPC wrapped ASAP envelopes
252
+ 2. Validates the request structure
253
+ 3. Extracts and processes the ASAP envelope
254
+ 4. Returns response wrapped in JSON-RPC
255
+ 5. Records metrics for observability
256
+
257
+ Args:
258
+ request: FastAPI request object with JSON body
259
+
260
+ Returns:
261
+ JSON-RPC response or error response
262
+
263
+ Example:
264
+ >>> response = await handler.handle_message(request)
265
+ """
266
+ start_time = time.perf_counter()
267
+ metrics = get_metrics()
268
+ payload_type = "unknown"
269
+
270
+ try:
271
+ # Parse JSON body
272
+ try:
273
+ body = await self.parse_json_body(request)
274
+ except ValueError as e:
275
+ # Invalid JSON - return parse error
276
+ error_response = self.build_error_response(
277
+ PARSE_ERROR,
278
+ data={"error": str(e)},
279
+ request_id=None,
280
+ )
281
+ self.record_error_metrics(metrics, "unknown", "parse_error", 0.0)
282
+ return error_response
283
+
284
+ # Validate body is a dict (JSON-RPC requires object at root)
285
+ if not isinstance(body, dict):
286
+ error_response = self.build_error_response(
287
+ INVALID_REQUEST,
288
+ data={
289
+ "error": "JSON-RPC request must be an object",
290
+ "received_type": type(body).__name__,
291
+ },
292
+ request_id=None,
293
+ )
294
+ self.record_error_metrics(metrics, "unknown", "invalid_request", 0.0)
295
+ return error_response
296
+
297
+ # Validate JSON-RPC request structure and method
298
+ rpc_request, validation_error = self.validate_jsonrpc_request(body)
299
+ if validation_error is not None:
300
+ self.record_error_metrics(metrics, "unknown", "invalid_request", 0.0)
301
+ return validation_error
302
+
303
+ # Type narrowing: rpc_request is not None here
304
+ if rpc_request is None:
305
+ # This should not happen if validate_jsonrpc_request is correct
306
+ # but guard against it for robustness
307
+ error_response = self.build_error_response(
308
+ INTERNAL_ERROR,
309
+ data={"error": "Internal validation error"},
310
+ request_id=None,
311
+ )
312
+ self.record_error_metrics(
313
+ metrics, "unknown", "internal_error", time.perf_counter() - start_time
314
+ )
315
+ return error_response
316
+
317
+ # Verify authentication if enabled
318
+ authenticated_agent_id: str | None = None
319
+ if self.auth_middleware is not None:
320
+ try:
321
+ authenticated_agent_id = await self.auth_middleware.verify_authentication(
322
+ request
323
+ )
324
+ except HTTPException as e:
325
+ # Authentication failed - return JSON-RPC error
326
+ logger.warning(
327
+ "asap.request.auth_failed",
328
+ status_code=e.status_code,
329
+ detail=e.detail,
330
+ )
331
+ # Map HTTP status to JSON-RPC error code
332
+ error_code = INVALID_REQUEST if e.status_code == 401 else INVALID_PARAMS
333
+ error_response = self.build_error_response(
334
+ error_code,
335
+ data={"error": str(e.detail), "status_code": e.status_code},
336
+ request_id=rpc_request.id,
337
+ )
338
+ self.record_error_metrics(
339
+ metrics, "unknown", "auth_failed", time.perf_counter() - start_time
340
+ )
341
+ return error_response
342
+
343
+ # Validate params is a dict before accessing
344
+ if not isinstance(rpc_request.params, dict):
345
+ logger.warning(
346
+ "asap.request.invalid_params_type",
347
+ params_type=type(rpc_request.params).__name__,
348
+ )
349
+ error_response = self.build_error_response(
350
+ INVALID_PARAMS,
351
+ data={
352
+ "error": "JSON-RPC 'params' must be an object",
353
+ "received_type": type(rpc_request.params).__name__,
354
+ },
355
+ request_id=rpc_request.id,
356
+ )
357
+ self.record_error_metrics(
358
+ metrics, "unknown", "invalid_params", time.perf_counter() - start_time
359
+ )
360
+ return error_response
361
+
362
+ # Extract envelope from params
363
+ envelope_data = rpc_request.params.get("envelope")
364
+ if envelope_data is None:
365
+ logger.warning("asap.request.missing_envelope")
366
+ error_response = self.build_error_response(
367
+ INVALID_PARAMS,
368
+ data={"error": "Missing 'envelope' in params"},
369
+ request_id=rpc_request.id,
370
+ )
371
+ self.record_error_metrics(
372
+ metrics, "unknown", "missing_envelope", time.perf_counter() - start_time
373
+ )
374
+ return error_response
375
+
376
+ # Validate envelope structure
377
+ try:
378
+ envelope = Envelope(**envelope_data)
379
+ payload_type = envelope.payload_type
380
+ except ValidationError as e:
381
+ logger.warning(
382
+ "asap.request.invalid_envelope",
383
+ error="Invalid envelope structure",
384
+ validation_errors=str(e.errors()),
385
+ )
386
+ duration_seconds = time.perf_counter() - start_time
387
+ self.record_error_metrics(
388
+ metrics, payload_type, "invalid_envelope", duration_seconds
389
+ )
390
+ return self.build_error_response(
391
+ INVALID_PARAMS,
392
+ data={
393
+ "error": "Invalid envelope structure",
394
+ "validation_errors": e.errors(),
395
+ },
396
+ request_id=rpc_request.id,
397
+ )
398
+
399
+ # Verify sender matches authenticated identity
400
+ if self.auth_middleware is not None:
401
+ try:
402
+ self.auth_middleware.verify_sender_matches_auth(
403
+ authenticated_agent_id, envelope.sender
404
+ )
405
+ except HTTPException as e:
406
+ # Sender mismatch - return JSON-RPC error
407
+ logger.warning(
408
+ "asap.request.sender_mismatch",
409
+ authenticated_agent=authenticated_agent_id,
410
+ envelope_sender=envelope.sender,
411
+ )
412
+ error_response = self.build_error_response(
413
+ INVALID_PARAMS,
414
+ data={"error": str(e.detail), "status_code": e.status_code},
415
+ request_id=rpc_request.id,
416
+ )
417
+ duration_seconds = time.perf_counter() - start_time
418
+ self.record_error_metrics(
419
+ metrics, payload_type, "sender_mismatch", duration_seconds
420
+ )
421
+ return error_response
422
+
423
+ # Log request received
424
+ logger.info(
425
+ "asap.request.received",
426
+ envelope_id=envelope.id,
427
+ trace_id=envelope.trace_id,
428
+ payload_type=envelope.payload_type,
429
+ sender=envelope.sender,
430
+ recipient=envelope.recipient,
431
+ authenticated=authenticated_agent_id is not None,
432
+ )
433
+
434
+ # Process the envelope using the handler registry
435
+ try:
436
+ response_envelope = await self.registry.dispatch_async(envelope, self.manifest)
437
+ except HandlerNotFoundError as e:
438
+ # No handler registered for this payload type
439
+ logger.warning(
440
+ "asap.request.handler_not_found",
441
+ payload_type=e.payload_type,
442
+ envelope_id=envelope.id,
443
+ )
444
+ # Record error metric
445
+ duration_seconds = time.perf_counter() - start_time
446
+ metrics.increment_counter(
447
+ "asap_requests_total",
448
+ {"payload_type": payload_type, "status": "error"},
449
+ )
450
+ metrics.increment_counter(
451
+ "asap_requests_error_total",
452
+ {"payload_type": payload_type, "error_type": "handler_not_found"},
453
+ )
454
+ metrics.observe_histogram(
455
+ "asap_request_duration_seconds",
456
+ duration_seconds,
457
+ {"payload_type": payload_type, "status": "error"},
458
+ )
459
+ handler_error = JsonRpcErrorResponse(
460
+ error=JsonRpcError.from_code(
461
+ METHOD_NOT_FOUND,
462
+ data={
463
+ "payload_type": e.payload_type,
464
+ "error": str(e),
465
+ },
466
+ ),
467
+ id=rpc_request.id,
468
+ )
469
+ return JSONResponse(
470
+ status_code=200,
471
+ content=handler_error.model_dump(),
472
+ )
473
+
474
+ # Calculate duration
475
+ duration_seconds = time.perf_counter() - start_time
476
+ duration_ms = duration_seconds * 1000
477
+
478
+ # Record success metrics
479
+ metrics.increment_counter(
480
+ "asap_requests_total",
481
+ {"payload_type": payload_type, "status": "success"},
482
+ )
483
+ metrics.increment_counter(
484
+ "asap_requests_success_total",
485
+ {"payload_type": payload_type},
486
+ )
487
+ metrics.observe_histogram(
488
+ "asap_request_duration_seconds",
489
+ duration_seconds,
490
+ {"payload_type": payload_type, "status": "success"},
491
+ )
492
+
493
+ # Log successful processing
494
+ logger.info(
495
+ "asap.request.processed",
496
+ envelope_id=envelope.id,
497
+ response_id=response_envelope.id,
498
+ trace_id=envelope.trace_id,
499
+ payload_type=envelope.payload_type,
500
+ duration_ms=round(duration_ms, 2),
501
+ )
502
+
503
+ # Wrap response in JSON-RPC
504
+ rpc_response = JsonRpcResponse(
505
+ result={"envelope": response_envelope.model_dump(mode="json")},
506
+ id=rpc_request.id,
507
+ )
508
+
509
+ return JSONResponse(
510
+ status_code=200,
511
+ content=rpc_response.model_dump(),
512
+ )
513
+
514
+ except Exception as e:
515
+ # Calculate duration for error case
516
+ duration_seconds = time.perf_counter() - start_time
517
+ duration_ms = duration_seconds * 1000
518
+
519
+ # Record error metrics
520
+ metrics.increment_counter(
521
+ "asap_requests_total",
522
+ {"payload_type": payload_type, "status": "error"},
523
+ )
524
+ metrics.increment_counter(
525
+ "asap_requests_error_total",
526
+ {"payload_type": payload_type, "error_type": "internal_error"},
527
+ )
528
+ metrics.observe_histogram(
529
+ "asap_request_duration_seconds",
530
+ duration_seconds,
531
+ {"payload_type": payload_type, "status": "error"},
532
+ )
533
+
534
+ # Log error
535
+ logger.exception(
536
+ "asap.request.error",
537
+ error=str(e),
538
+ error_type=type(e).__name__,
539
+ duration_ms=round(duration_ms, 2),
540
+ )
541
+
542
+ # Internal server error
543
+ internal_error = JsonRpcErrorResponse(
544
+ error=JsonRpcError.from_code(
545
+ INTERNAL_ERROR,
546
+ data={"error": str(e), "type": type(e).__name__},
547
+ ),
548
+ id=None,
549
+ )
550
+ return JSONResponse(
551
+ status_code=200,
552
+ content=internal_error.model_dump(),
553
+ )
554
+
555
+
556
+ def create_app(
557
+ manifest: Manifest,
558
+ registry: HandlerRegistry | None = None,
559
+ token_validator: Callable[[str], str | None] | None = None,
560
+ ) -> FastAPI:
561
+ """Create and configure a FastAPI application for ASAP protocol.
562
+
563
+ This factory function creates a FastAPI app with:
564
+ - POST /asap endpoint for handling ASAP messages via JSON-RPC
565
+ - GET /.well-known/asap/manifest.json for agent discovery
566
+ - GET /asap/metrics for Prometheus-compatible metrics
567
+ - Authentication middleware (if manifest.auth is configured)
568
+ - Error handling middleware
569
+ - Request validation
570
+ - Extensible handler registry for payload processing
571
+
572
+ Args:
573
+ manifest: The agent's manifest describing capabilities and endpoints
574
+ registry: Optional handler registry for processing payloads.
575
+ If None, a default registry with echo handler is created.
576
+ token_validator: Optional function to validate Bearer tokens.
577
+ Required if manifest.auth is configured. Should return agent ID
578
+ if token is valid, None otherwise.
579
+
580
+ Returns:
581
+ Configured FastAPI application ready to run
582
+
583
+ Raises:
584
+ ValueError: If manifest requires authentication but no token_validator provided
585
+
586
+ Example:
587
+ >>> from asap.models.entities import Manifest, Capability, Endpoint, Skill, AuthScheme
588
+ >>> from asap.transport.handlers import HandlerRegistry
589
+ >>> manifest = Manifest(
590
+ ... id="urn:asap:agent:test",
591
+ ... name="Test Agent",
592
+ ... version="1.0.0",
593
+ ... description="Test agent",
594
+ ... capabilities=Capability(
595
+ ... asap_version="0.1",
596
+ ... skills=[Skill(id="test", description="Test skill")],
597
+ ... state_persistence=False
598
+ ... ),
599
+ ... endpoints=Endpoint(asap="http://localhost:8000/asap")
600
+ ... )
601
+ >>> app = create_app(manifest)
602
+ >>>
603
+ >>> # With authentication:
604
+ >>> manifest_with_auth = Manifest(
605
+ ... ..., # same as above
606
+ ... auth=AuthScheme(schemes=["bearer"])
607
+ ... )
608
+ >>> def my_token_validator(token: str) -> str | None:
609
+ ... if token == "valid-token":
610
+ ... return "urn:asap:agent:client"
611
+ ... return None
612
+ >>> app = create_app(manifest_with_auth, token_validator=my_token_validator)
613
+ >>>
614
+ >>> # With custom registry:
615
+ >>> registry = HandlerRegistry()
616
+ >>> registry.register("task.request", my_handler)
617
+ >>> app = create_app(manifest, registry)
618
+ >>> # Run with uvicorn: uvicorn module:app
619
+ """
620
+ # Use default registry if none provided
621
+ if registry is None:
622
+ registry = create_default_registry()
623
+
624
+ # Create authentication middleware if auth is configured
625
+ auth_middleware: AuthenticationMiddleware | None = None
626
+ if manifest.auth is not None:
627
+ if token_validator is None:
628
+ raise ValueError(
629
+ "token_validator is required when manifest.auth is configured. "
630
+ "Provide a function that validates tokens and returns agent IDs."
631
+ )
632
+ validator = BearerTokenValidator(token_validator)
633
+ auth_middleware = AuthenticationMiddleware(manifest, validator)
634
+ logger.info(
635
+ "asap.server.auth_enabled",
636
+ manifest_id=manifest.id,
637
+ schemes=manifest.auth.schemes,
638
+ )
639
+
640
+ # Create request handler
641
+ handler = ASAPRequestHandler(registry, manifest, auth_middleware)
642
+
643
+ app = FastAPI(
644
+ title="ASAP Protocol Server",
645
+ description=f"ASAP server for {manifest.name}",
646
+ version=manifest.version,
647
+ )
648
+ # Note: Request size limits should be configured at the ASGI server level (e.g., uvicorn).
649
+ # For production, consider setting --limit-max-requests or using a reverse proxy
650
+ # (nginx, traefik) to enforce request size limits (e.g., 10MB max).
651
+
652
+ @app.get("/.well-known/asap/manifest.json")
653
+ async def get_manifest() -> dict[str, Any]:
654
+ """Return the agent's manifest for discovery.
655
+
656
+ This endpoint allows other agents to discover this agent's
657
+ capabilities, skills, and communication endpoints.
658
+
659
+ Returns:
660
+ Agent manifest as JSON dictionary
661
+
662
+ Example:
663
+ >>> manifest = get_manifest()
664
+ >>> "id" in manifest
665
+ True
666
+ """
667
+ return manifest.model_dump()
668
+
669
+ @app.get("/asap/metrics")
670
+ async def get_metrics_endpoint() -> PlainTextResponse:
671
+ """Return Prometheus-compatible metrics.
672
+
673
+ This endpoint exposes server metrics in Prometheus text format,
674
+ including request counts, error rates, and latency histograms.
675
+
676
+ Returns:
677
+ PlainTextResponse with metrics in Prometheus format
678
+
679
+ Example:
680
+ curl http://localhost:8000/asap/metrics
681
+ """
682
+ metrics = get_metrics()
683
+ return PlainTextResponse(
684
+ content=metrics.export_prometheus(),
685
+ media_type="text/plain; version=0.0.4; charset=utf-8",
686
+ )
687
+
688
+ @app.post("/asap")
689
+ async def handle_asap_message(request: Request) -> JSONResponse:
690
+ """Handle ASAP messages wrapped in JSON-RPC 2.0.
691
+
692
+ This endpoint:
693
+ 1. Receives JSON-RPC wrapped ASAP envelopes
694
+ 2. Validates the request structure
695
+ 3. Extracts and processes the ASAP envelope
696
+ 4. Returns response wrapped in JSON-RPC
697
+ 5. Records metrics for observability
698
+
699
+ Args:
700
+ request: FastAPI request object with JSON body
701
+
702
+ Returns:
703
+ JSON-RPC response or error response
704
+
705
+ Example:
706
+ >>> # Send JSON-RPC to POST /asap and receive JSON-RPC response.
707
+ >>> # See tests/transport/test_server.py for full request examples.
708
+ """
709
+ return await handler.handle_message(request)
710
+
711
+ return app
712
+
713
+
714
+ def _create_default_manifest() -> Manifest:
715
+ """Create a default manifest for standalone server execution.
716
+
717
+ This manifest is used when running the server directly via uvicorn
718
+ without providing a custom manifest.
719
+
720
+ Returns:
721
+ Default manifest with basic echo capabilities
722
+ """
723
+ return Manifest(
724
+ id="urn:asap:agent:default-server",
725
+ name="ASAP Default Server",
726
+ version="0.1.0",
727
+ description="Default ASAP protocol server with echo capabilities",
728
+ capabilities=Capability(
729
+ asap_version="0.1",
730
+ skills=[Skill(id="echo", description="Echo back the input")],
731
+ state_persistence=False,
732
+ ),
733
+ endpoints=Endpoint(asap="http://localhost:8000/asap"),
734
+ )
735
+
736
+
737
+ # Default app instance for direct uvicorn execution:
738
+ # uvicorn asap.transport.server:app --host 0.0.0.0 --port 8000
739
+ app = create_app(_create_default_manifest(), create_default_registry())