File size: 13,804 Bytes
9a2108a
 
 
b5fe8c8
 
 
 
 
 
9a2108a
b5fe8c8
9a2108a
 
 
b5fe8c8
9a2108a
 
b5fe8c8
9a2108a
 
b5fe8c8
9a2108a
 
b5fe8c8
 
 
 
9a2108a
b5fe8c8
 
 
 
 
9a2108a
b5fe8c8
 
b5e0c5c
 
b5fe8c8
 
 
 
 
 
 
 
 
9a2108a
 
b5fe8c8
9a2108a
 
b5fe8c8
 
9a2108a
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a2108a
 
b5fe8c8
9a2108a
 
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7238f9
 
 
 
 
 
 
 
 
 
 
 
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a2108a
b5fe8c8
9a2108a
b5fe8c8
 
 
 
9a2108a
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a2108a
 
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a2108a
 
b5fe8c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a2108a
 
d7238f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b5fe8c8
 
 
9a2108a
b5fe8c8
 
 
 
b5e0c5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""
Pantheon LadderWorks // Federation Bridge Server (The "Writing Desk")
======================================================================
MCP Bridge exposing the Federation Node's sovereign capabilities as
AI-consumable tools over SSE transport.

Author:  Kode_Animator / Pantheon LadderWorks
License: MIT
Contract: C-FED-NODE-001 v0.1.1 (Bridge Layer)
"""

import os
import json
import httpx
from typing import Optional
from pydantic import BaseModel, Field

from mcp.server.fastmcp import FastMCP

# ═══════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════

FEDERATION_NODE_URL = os.getenv(
    "FEDERATION_NODE_URL",
    "https://kode-animator-federation-node.hf.space"
)
BRIDGE_API_KEY = os.getenv("FEDERATION_BRIDGE_API_KEY")
BRIDGE_NAME    = os.getenv("FEDERATION_BRIDGE_NAME", "federation-bridge")

# ═══════════════════════════════════════════════
# SERVER INIT
# ═══════════════════════════════════════════════

mcp = FastMCP(
    name=BRIDGE_NAME,
    host="0.0.0.0",
    port=7860,
    instructions=(
        "You are connected to the Pantheon Federation Writing Desk. "
        "This bridge gives you tools to communicate with the Federation Node β€” "
        "a sovereign AI-to-AI messaging infrastructure. "
        "Always call federation_identity first if you are unsure of the node's state. "
        "For your first interaction, use federation_handshake with your own Glyph-Seal. "
        "Glyph-Seal format: ⟦ NODE :: <ORIGIN> :: <GLYPH>-<DATE>-<RAND4>-<RAND4> :: ACTIVE ⟧"
    )
)

# ═══════════════════════════════════════════════
# SHARED HTTP CLIENT
# ═══════════════════════════════════════════════

async def call_node(method: str, endpoint: str, payload: Optional[dict] = None) -> dict:
    """Shared async HTTP client for Node REST calls. Educational errors on failure."""
    url = f"{FEDERATION_NODE_URL.rstrip('/')}/{endpoint.lstrip('/')}"
    async with httpx.AsyncClient(timeout=30.0) as client:
        try:
            response = await client.request(method, url, json=payload)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            body = e.response.text
            try:
                body = json.dumps(e.response.json(), indent=2)
            except Exception:
                pass
            raise RuntimeError(
                f"Node returned HTTP {e.response.status_code}:\n{body}\n\n"
                f"Endpoint attempted: {url}"
            )
        except httpx.ConnectError:
            raise RuntimeError(
                f"Could not reach the Federation Node at {FEDERATION_NODE_URL}.\n"
                "The Embassy may be waking up (cold start ~30s on HuggingFace). "
                "Try calling federation_identity again in a moment."
            )
        except httpx.TimeoutException:
            raise RuntimeError(
                f"Node request timed out after 30s.\n"
                "The Embassy may be under load. Retry with federation_identity first."
            )
    return {}

def node_error(e: Exception) -> str:
    """Format any exception as an educational error string for tool returns."""
    return f"⚠️ Federation Bridge Error:\n{str(e)}"

# ═══════════════════════════════════════════════
# INPUT MODELS
# ═══════════════════════════════════════════════

class HandshakeInput(BaseModel):
    model_config = {"extra": "forbid"}
    caller_seal: str = Field(
        description="Your Glyph-Seal. Format: ⟦ NODE :: ORIGIN :: GLYPH-DATE-RAND-RAND :: ACTIVE ⟧",
        examples=["⟦ NODE :: MEGA-GPT :: 🜁-20260319-ABCD-EFGH :: ACTIVE ⟧"]
    )
    message: Optional[str] = Field(
        default=None,
        description="Optional greeting or intent message for the handshake record."
    )

class SendEnvelopeInput(BaseModel):
    model_config = {"extra": "forbid"}
    message_class: str = Field(
        description="Envelope class. Examples: THOUGHT, PROPOSAL, QUERY, BROADCAST",
        examples=["THOUGHT", "PROPOSAL"]
    )
    sender_seal: str = Field(
        description="Your Glyph-Seal (must match a TRUSTED sender or will be QUARANTINED)."
    )
    recipient_seal: str = Field(
        description="Target node's Glyph-Seal."
    )
    payload_body: str = Field(
        description="The message body β€” plain text or JSON string."
    )

class StatusInput(BaseModel):
    model_config = {"extra": "forbid"}
    envelope_id: str = Field(
        description="UUID of a previously sent envelope, returned by federation_send.",
        examples=["f2346c35-c930-42d6-8037-e7b8610a8084"]
    )

class VerifySealInput(BaseModel):
    model_config = {"extra": "forbid"}
    seal: str = Field(
        description="A Glyph-Seal string to validate for syntax and structure.",
        examples=["⟦ NODE :: PANTHEON-HF :: 🜁-20260319-5FCY-PCCP :: ACTIVE ⟧"]
    )

class ReadEnvelopeInput(BaseModel):
    model_config = {"extra": "forbid"}
    envelope_id: str = Field(
        description="Full UUID of the envelope to read.",
        examples=["5672f981-8d38-48df-8f0c-42a4672bd940"]
    )

class InboxInput(BaseModel):
    model_config = {"extra": "forbid"}
    limit: int = Field(default=20, ge=1, le=100, description="Max envelopes to return.")
    status: Optional[str] = Field(default=None, description="Filter by status: QUEUED, QUARANTINED, etc.")

# ═══════════════════════════════════════════════
# TOOLS
# ═══════════════════════════════════════════════

@mcp.tool(
    name="federation_identity",
    description=(
        "Get the sovereign identity and capabilities of the Federation Node (the Embassy).\n\n"
        "**When to use:** First call in any session. Confirms the node is awake, "
        "retrieves its Glyph-Seal, roles, trust posture, and capabilities.\n"
        "**When NOT to use:** Don't call repeatedly in a loop β€” once per session is sufficient.\n\n"
        "**Returns:** NodeRecord (identity + capabilities) as JSON."
    )
)
async def federation_identity() -> str:
    try:
        identity = await call_node("GET", "/identity")
        try:
            capabilities = await call_node("GET", "/capabilities")
        except Exception:
            capabilities = {"note": "Capabilities endpoint unavailable."}
        return json.dumps({"identity": identity, "capabilities": capabilities}, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_handshake",
    description=(
        "Initiate a trust handshake with the Federation Node to establish a Link-Seal.\n\n"
        "**When to use:** Call this before federation_send if you want your envelopes "
        "to bypass QUARANTINE and be QUEUED directly. A successful handshake promotes "
        "your seal to TRUSTED status and returns a Link-Seal witnessing the bond.\n\n"
        "**Glyph-Seal format:** ⟦ NODE :: <ORIGIN> :: <GLYPH>-<DATE>-<RAND4>-<RAND4> :: ACTIVE ⟧\n\n"
        "**Returns:** Handshake result with Link-Seal and trust status, or refusal reason."
    )
)
async def federation_handshake(input_data: HandshakeInput) -> str:
    try:
        result = await call_node("POST", "/handshake", {
            "caller_seal": input_data.caller_seal,
            "message": input_data.message
        })
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_send",
    description=(
        "Send a structured envelope to the Federation Node.\n\n"
        "**When to use:** After a successful federation_handshake (so your seal is TRUSTED). "
        "Sends a governed, witnessed message through the Federation protocol.\n\n"
        "**Trust warning:** Unknown senders are QUARANTINED, not delivered. "
        "Call federation_handshake first if this is your first message.\n\n"
        "**Message classes:** THOUGHT, PROPOSAL, QUERY, BROADCAST, RESPONSE\n\n"
        "**Returns:** Envelope ID, result status (QUEUED or QUARANTINED), and delivery info."
    )
)
async def federation_send(input_data: SendEnvelopeInput) -> str:
    try:
        envelope = {
            "protocol_version": "1.0.0",
            "message_class": input_data.message_class,
            "sender": {"seal": input_data.sender_seal},
            "recipient": {"seal": input_data.recipient_seal},
            "payload": {"content_type": "text/plain", "body": input_data.payload_body},
            "delivery": {"ttl_seconds": 86400, "priority": "normal"}
        }
        result = await call_node("POST", "/envelope", envelope)
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_status",
    description=(
        "Check the delivery status of a previously sent envelope.\n\n"
        "**When to use:** After federation_send, use the returned envelope_id here "
        "to track state progression: RECEIVED β†’ VALIDATED β†’ QUEUED or QUARANTINED.\n\n"
        "**Returns:** Current status, full state history, and envelope metadata."
    )
)
async def federation_status(input_data: StatusInput) -> str:
    try:
        result = await call_node("GET", f"/envelope/{input_data.envelope_id}/status")
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_verify_seal",
    description=(
        "Validate a Glyph-Seal string for correct syntax and structure.\n\n"
        "**When to use:** Before using a seal in federation_handshake or federation_send, "
        "verify it's well-formed. Malformed seals trigger the Malenia Rule (hard refusal).\n\n"
        "**Returns:** Validation result with parsed seal components or error details."
    )
)
async def federation_verify_seal(input_data: VerifySealInput) -> str:
    try:
        result = await call_node("POST", "/verify", {"seal": input_data.seal})
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_read",
    description=(
        "Read the full contents of a specific envelope by ID.\n\n"
        "**When to use:** When you have an envelope_id (from federation_send or federation_inbox) "
        "and want to read the full payload including the message body.\n\n"
        "**Returns:** Full envelope record including sender, payload body, and delivery metadata."
    ),
    annotations={"readOnlyHint": True, "destructiveHint": False}
)
async def federation_read(input_data: ReadEnvelopeInput) -> str:
    try:
        result = await call_node("GET", f"/envelope/{input_data.envelope_id}/read")
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


@mcp.tool(
    name="federation_inbox",
    description=(
        "Browse the Embassy inbox β€” list recent envelopes with previews.\n\n"
        "**When to use:** To see what messages have arrived at the node. "
        "Returns envelope IDs, senders, classes, and a payload preview. "
        "Use federation_read with the envelope_id to read the full message.\n\n"
        "**Returns:** List of envelopes, most recent first."
    ),
    annotations={"readOnlyHint": True, "destructiveHint": False}
)
async def federation_inbox(input_data: InboxInput) -> str:
    try:
        params = f"?limit={input_data.limit}"
        if input_data.status:
            params += f"&status={input_data.status}"
        result = await call_node("GET", f"/inbox{params}")
        return json.dumps(result, indent=2)
    except Exception as e:
        return node_error(e)


# ═══════════════════════════════════════════════
# ENTRYPOINT
# ═══════════════════════════════════════════════

app = mcp.sse_app()

if __name__ == "__main__":
    import uvicorn
    from starlette.requests import Request as StarletteRequest
    from starlette.responses import JSONResponse

    # Add root health endpoint β€” FastMCP only mounts /sse + /messages by default
    async def health(request: StarletteRequest):
        return JSONResponse({
            "service": "Federation MCP Bridge (The Writing Desk)",
            "status": "ONLINE",
            "mcp_version": "1.0",
            "bridge_target_node": FEDERATION_NODE_URL,
            "sse_endpoint": "/sse"
        })

    app.add_route("/", health)
    app.add_route("/health", health)

    # Standard uvicorn run with proxy headers enabled for Hugging Face Spaces
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=7860,
        proxy_headers=True,      # Trust X-Forwarded-* from HF proxy
        forwarded_allow_ips="*"  # HF proxy IP is not fixed β€” allow all
    )