eclipse-gig-core 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.
bazaar-manifest.json ADDED
@@ -0,0 +1 @@
1
+ {"bench":{"bench_verdict":"CERTIFIED(local)","certified":true,"certified_scope":"local","checks":["exact-address brief refused (ZIP-tier only)","approval binds the exact brief hash","post-approval edit invalidates approval","UNEXPOSED-fee quote never hireable under a ceiling","owner exception: all-in computed, status stays UNEXPOSED","mandate ALLOW without person token is not hireable","platform-done vs owner-not_done opens an exception; states never merge","review draft carries no quality claims the record lacks","leverage language refused at draft time","capability denylist: exactly the 10 G0 tools","receipt chain valid; tamper detected","auth enforced","schema valid","no network surface (local-only build)","no secret leak","sane errors","process health","audit completeness","bench isolation"],"not_tested":["Live platform calls of any kind — G0 contains no live marketplace code path; briefs, quotes, mandates, and verifications are local records evaluated against shipped test data.","Adapter variant approval binding against a live adapter — the Compass fixture adapter is the first consumer; cross-product bench is a later run.","Photo EXIF stripping tooling itself — the core records the strip status per photo as supplied; the stripping tool is adapter/build scope [verify] per spec 5.1."],"scorecard_ref":"test-bench/results/eclipse-gig-core-mcp-2026-10-25.json","status":"CERTIFIED(local) — Eclipse Bazaar Test Bench 2026-10-25, 18/19 checks pass, 1 na (U1 auth: stdio process boundary). Run by W-003 (Verity), suites/gig_core.py."},"category":"Gig / Task Marketplace Infrastructure","created":"2026-10-25","description":"The shared core of the Eclipse gig adapter family: one canonical, owner-approved Task Brief (versioned, SHA-256 hash-bound; any post-approval edit invalidates approval) becomes a native request on each task marketplace through its adapter. The core normalizes quotes from every platform model (lead, stated-budget, fixed-price, hourly) to one record with face price and itemized fees forever separate and a single ALL-IN total; a quote whose fees are UNEXPOSED cannot be hired under a ceiling absent a written per-quote owner exception. The hire gate evaluates a live Eclipse Procure / Agent Wallet mandate (cap, category + platform allowlists, approval threshold, expiry) and returns receipted ALLOW/DENY decisions; in v1, selecting a person requires an owner-confirm token even under a mandate. Completion verification keeps platform state and owner attestation in separate fields (platform-done is not verified), and review drafts are generated only from the verification record. G0 executes nothing and calls nothing live.","icon":"icon-512.png","name":"Eclipse Gig Core MCP","powered_by":"Cyan Eclipse Technologies","price_model":"Free local v0.1; family pricing TBD per spec","repo":null,"slug":"eclipse-gig-core-mcp","status":"In development — v0.1.0 G0 local build (Task Brief core + quote normalization + hire gate evaluation; fixtures only; CERTIFIED(local) — Eclipse Bazaar Test Bench 2026-10-25)","tagline":"One approved Task Brief, every marketplace: hash-bound briefs, all-in quotes, and a hire that runs only inside a mandate.","tools":["brief_create","brief_update","brief_approve","brief_read","quote_normalize","quote_read","hire_check","completion_verify","review_draft","receipt_list"],"transport":"MCP over stdio (HTTP transport on roadmap)","vendor":"Cyan Eclipse Technologies","x402_ready":false}
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.5
2
+ Name: eclipse-gig-core
3
+ Version: 0.1.0
4
+ Summary: Eclipse Gig Core MCP — the shared core of the Eclipse gig adapter family: one canonical, owner-approved, hash-bound Task Brief; quote normalization to face price + itemized fees + one all-in total; and the mandate hire gate. Fixtures and evaluation only: no live platform call, no hire executed, no payment moved.
5
+ Project-URL: Homepage, https://cetech.ai
6
+ Project-URL: Repository, https://github.com/CYAN-ECLIPSE-TECHNOLOGIES-LLC/eclipse-gig-core-mcp
7
+ Project-URL: Vendor, https://cetech.ai
8
+ Author: Cyan Eclipse Technologies LLC
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: gig-economy,mcp,model-context-protocol,procurement,task-marketplace
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # eclipse-gig-core
21
+
22
+ mcp-name: io.github.CYAN-ECLIPSE-TECHNOLOGIES-LLC/gig-core
23
+
24
+ **What this is:** the MCP server for **Eclipse Gig Core** — the shared
25
+ core of Cyan Eclipse Technologies' Eclipse gig adapter family. One
26
+ canonical, owner-approved Task Brief becomes a native request, offer
27
+ round, booking, or prep pack on each task marketplace, according to
28
+ what that platform actually sanctions.
29
+
30
+ **What this is not:** phase G0 is fixtures and evaluation only. **No
31
+ live platform call exists in this code, no hire is executed, no review
32
+ is submitted, and no payment moves.** Hiring happens only inside an
33
+ Eclipse Procure / Agent Wallet mandate, evaluated — never executed —
34
+ by this core.
35
+
36
+ ## Run it
37
+
38
+ ```console
39
+ $ pip install eclipse-gig-core
40
+ $ eclipse-gig-core # MCP over stdio, Python stdlib only
41
+ ```
42
+
43
+ MCP client config:
44
+
45
+ ```json
46
+ { "mcpServers": { "eclipse-gig-core": { "command": "uvx", "args": ["eclipse-gig-core"] } } }
47
+ ```
48
+
49
+ ## Tools (10)
50
+
51
+ `brief_create` · `brief_update` · `brief_approve` · `brief_read` ·
52
+ `quote_normalize` · `quote_read` · `hire_check` · `completion_verify` ·
53
+ `review_draft` · `receipt_list`
54
+
55
+ ## Honest scope
56
+
57
+ - The Task Brief is versioned and SHA-256 hash-bound; any
58
+ post-approval edit invalidates the approval. Quotes normalize every
59
+ platform model (lead, stated-budget, fixed-price, hourly) to face
60
+ price and itemized fees — forever separate — plus one all-in total;
61
+ a quote whose fees are unexposed cannot be hireable under a ceiling.
62
+ - The hire gate returns ALLOW / DENY with named reasons, receipted
63
+ either way; in v1, selecting a person requires an owner-confirm
64
+ token even when a mandate allows the money.
65
+ - State persists in `data/state.json` beside the server (override with
66
+ `ECLIPSE_GIG_CORE_STATE`).
67
+ - Certified (local) by the Eclipse Test Bench at v0.1.0; the listing
68
+ status in the Eclipse Bazaar catalog is in-development.
69
+
70
+ Powered by Cyan Eclipse Technologies LLC (cetech.ai) — a Veteran-Owned
71
+ Small Business. MIT licensed.
@@ -0,0 +1,7 @@
1
+ bazaar-manifest.json,sha256=ZHINaFHszZXpx4JIZzhj3h9_jyHQ7YpR2Z1tgBOejs8,3474
2
+ server.py,sha256=0Lnks3qs7IswwDrFTMjYU4tYUoC8ltAn9z6OoEsoypg,37125
3
+ eclipse_gig_core-0.1.0.dist-info/METADATA,sha256=dceSYh9v_j974HtUfcufvZSfOKoMZAu6xLJUrB5IjGA,3025
4
+ eclipse_gig_core-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ eclipse_gig_core-0.1.0.dist-info/entry_points.txt,sha256=95dyQ5aC-s4cw1kFnOlxPOZjJDAQ1QHwdPNQpbnDeXc,49
6
+ eclipse_gig_core-0.1.0.dist-info/licenses/LICENSE,sha256=J64raxD33SRnnCn8p1F-lRl0rnesurM9Jlkd3auDZvA,1086
7
+ eclipse_gig_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eclipse-gig-core = server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyan Eclipse Technologies LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
server.py ADDED
@@ -0,0 +1,855 @@
1
+ #!/usr/bin/env python3
2
+ # Eclipse Gig Core MCP
3
+ # Copyright © 2026 Cyan Eclipse Technologies LLC · cetech.ai
4
+
5
+ """Eclipse Gig Core MCP — v0.1.0, phase G0 (shared Task Brief core).
6
+
7
+ Powered by Cyan Eclipse Technologies (cetech.ai).
8
+
9
+ The Eclipse gig adapter family — one canonical, owner-approved Task
10
+ Brief becomes a native request, offer round, booking, or prep pack on
11
+ each task marketplace, according to what that platform actually
12
+ sanctions. Hiring happens only inside an Eclipse Procure / Agent
13
+ Wallet mandate. It never diverts a hire off-platform, never invents
14
+ a sanctioned surface that does not exist, and never posts a brief
15
+ the owner has not approved.
16
+
17
+ This server is the SHARED CORE (spec eclipse-gig-family-spec-20261025
18
+ sections 5-7). It owns:
19
+ * the canonical Task Brief object — versioned, SHA-256 hash-bound,
20
+ owner-approved; any post-approval edit invalidates approval;
21
+ * the quote normalization engine — every platform model (lead,
22
+ stated-budget, fixed-price, hourly) normalizes to one record with
23
+ face price and itemized fees FOREVER separate and one ALL-IN
24
+ total; a quote whose fees are UNEXPOSED cannot be hireable under
25
+ a ceiling;
26
+ * the hire gate — a mandate evaluation (cap, category + platform
27
+ allowlists, approval threshold, expiry) returning ALLOW/DENY,
28
+ receipted either way. In v1, selecting a PERSON requires an
29
+ owner-confirm token even when a mandate allows the money;
30
+ * completion verification records — platform state and owner
31
+ verification are separate fields and are never merged;
32
+ * review drafts generated ONLY from a verification record.
33
+
34
+ G0 SCOPE: fixtures and evaluation only. NO live platform call exists
35
+ in this file, no hire is executed, no review is submitted, and no
36
+ payment moves. The platform is merchant of record on every real hire
37
+ (spec section 6); this core only evaluates and records.
38
+
39
+ Transport: MCP over stdio (newline-delimited JSON-RPC 2.0), stdlib only.
40
+ State: $ECLIPSE_GIG_CORE_STATE or ./data/state.json beside this file.
41
+ """
42
+ import hashlib
43
+ import json
44
+ import os
45
+ import re
46
+ import sys
47
+ import time
48
+ from datetime import datetime, timezone
49
+ from pathlib import Path
50
+
51
+ HERE = Path(__file__).parent
52
+ STATE_PATH = Path(os.environ.get(
53
+ "ECLIPSE_GIG_CORE_STATE", HERE / "data" / "state.json"))
54
+
55
+ BRIEF_CONTENT_FIELDS = [
56
+ "category", "service", "scope", "location", "timing",
57
+ "ceiling_all_in", "tip_policy", "photos", "license_required",
58
+ "safety_access", "intended_platforms", "prohibited_platforms",
59
+ "details",
60
+ ]
61
+
62
+ QUOTE_MODELS = {"lead_model", "stated_budget", "fixed_price", "hourly"}
63
+
64
+ # Words a review draft may only contain when the verification record
65
+ # itself carries quality notes (spec 7.3: no sentiment not in record).
66
+ QUALITY_CLAIM_WORDS = ["excellent", "great", "superb", "outstanding",
67
+ "fantastic", "quality", "wonderful", "amazing"]
68
+
69
+ # Prohibited leverage language, filtered at draft time (spec 7.3).
70
+ LEVERAGE_PATTERNS = ["refund or", "or i will post", "or i'll post",
71
+ "take this down", "or else i", "one star unless",
72
+ "1 star unless", "negative review unless"]
73
+
74
+
75
+ class ToolError(Exception):
76
+ pass
77
+
78
+
79
+ def now_iso() -> str:
80
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
81
+
82
+
83
+ def parse_time(value):
84
+ if value is None:
85
+ return None
86
+ text = str(value).strip()
87
+ if len(text) == 10:
88
+ text += "T00:00:00+00:00"
89
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
90
+
91
+
92
+ def sha(text: str) -> str:
93
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
94
+
95
+
96
+ def canon(obj) -> str:
97
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"))
98
+
99
+
100
+ def money(value) -> float:
101
+ return round(float(value), 2)
102
+
103
+
104
+ # --------------------------------------------------------------------- state
105
+ def empty_state() -> dict:
106
+ return {"briefs": {}, "quotes": {}, "verifications": {},
107
+ "receipts": []}
108
+
109
+
110
+ def load_state() -> dict:
111
+ if STATE_PATH.exists():
112
+ return json.loads(STATE_PATH.read_text())
113
+ return empty_state()
114
+
115
+
116
+ def save_state(state: dict) -> None:
117
+ STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
118
+ STATE_PATH.write_text(json.dumps(state, indent=1))
119
+
120
+
121
+ # ------------------------------------------------------------------ receipts
122
+ def emit_receipt(state: dict, rtype: str, detail: dict) -> dict:
123
+ """Append-only, hash-chained (house pattern)."""
124
+ prev = state["receipts"][-1]["hash"] if state["receipts"] else "GENESIS"
125
+ entry = {"seq": len(state["receipts"]) + 1, "ts": int(time.time()),
126
+ "type": rtype, "prev_hash": prev, "detail": detail}
127
+ entry["hash"] = sha(prev + canon(
128
+ {k: v for k, v in entry.items() if k != "hash"}))
129
+ state["receipts"].append(entry)
130
+ return entry
131
+
132
+
133
+ def verify_receipt_chain(receipts) -> bool:
134
+ prev = "GENESIS"
135
+ for entry in receipts:
136
+ if entry.get("prev_hash") != prev:
137
+ return False
138
+ expected = sha(prev + canon(
139
+ {k: v for k, v in entry.items() if k != "hash"}))
140
+ if entry.get("hash") != expected:
141
+ return False
142
+ prev = entry["hash"]
143
+ return True
144
+
145
+
146
+ # --------------------------------------------------------------------- briefs
147
+ def brief_content(brief: dict) -> dict:
148
+ return {k: brief.get(k) for k in BRIEF_CONTENT_FIELDS}
149
+
150
+
151
+ def brief_hash(brief: dict) -> str:
152
+ return sha(canon(brief_content(brief)))
153
+
154
+
155
+ def validate_brief_content(content: dict) -> None:
156
+ if not str(content.get("category") or "").strip():
157
+ raise ToolError("brief requires a category")
158
+ if not str(content.get("service") or "").strip():
159
+ raise ToolError("brief requires a service")
160
+ scope = content.get("scope") or {}
161
+ if not str(scope.get("statement") or "").strip():
162
+ raise ToolError("brief requires scope.statement "
163
+ "(what done looks like)")
164
+ location = content.get("location") or {}
165
+ precision = str(location.get("precision") or "zip").lower()
166
+ if precision != "zip":
167
+ raise ToolError(
168
+ "LOCATION_PRECISION: briefs carry ZIP-tier location only; "
169
+ "the exact address is released only inside a platform's "
170
+ "own post-hire flow (spec 5.1/8.4)")
171
+ if not str(location.get("zip") or "").strip():
172
+ raise ToolError("brief requires location.zip (ZIP-tier)")
173
+ timing = content.get("timing") or {}
174
+ if not timing.get("window_start") or not timing.get("window_end"):
175
+ raise ToolError("brief requires timing.window_start/window_end")
176
+ ceiling = content.get("ceiling_all_in")
177
+ if ceiling is None or float(ceiling) <= 0:
178
+ raise ToolError("brief requires a positive ceiling_all_in "
179
+ "(one ALL-IN number the owner authorizes)")
180
+ license_required = content.get("license_required")
181
+ if license_required not in ("unknown", "no") and not (
182
+ isinstance(license_required, str)
183
+ and license_required.startswith("yes:")):
184
+ raise ToolError("license_required must be 'unknown', 'no', or "
185
+ "'yes:<type>' (spec 5.1)")
186
+
187
+
188
+ def public_brief(brief: dict) -> dict:
189
+ out = json.loads(json.dumps(brief))
190
+ out["approval_valid"] = bool(
191
+ brief.get("approval")
192
+ and brief["approval"].get("brief_hash") == brief["hash"])
193
+ return out
194
+
195
+
196
+ def t_brief_create(state, args):
197
+ content = {k: args.get(k) for k in BRIEF_CONTENT_FIELDS}
198
+ content["photos"] = content.get("photos") or []
199
+ content["intended_platforms"] = content.get("intended_platforms") or []
200
+ content["prohibited_platforms"] = \
201
+ content.get("prohibited_platforms") or []
202
+ content["details"] = content.get("details") or {}
203
+ validate_brief_content(content)
204
+ photos = []
205
+ for photo in content["photos"]:
206
+ photos.append({
207
+ "ref": str(photo.get("ref", "")),
208
+ # EXIF/geolocation stripping is recorded on the photo
209
+ # record itself; unstripped photos are flagged, and an
210
+ # adapter must not use them (spec 5.1).
211
+ "exif_stripped": bool(photo.get("exif_stripped", False)),
212
+ "exif_note": ("EXIF/geolocation stripped before adapter use"
213
+ if photo.get("exif_stripped")
214
+ else "NOT stripped — adapter use blocked"),
215
+ })
216
+ content["photos"] = photos
217
+ brief_id = str(args.get("brief_id") or "")
218
+ if not brief_id:
219
+ brief_id = "brief_" + sha(canon(content) + now_iso())[:12]
220
+ if brief_id in state["briefs"]:
221
+ raise ToolError(f"brief already exists: {brief_id}")
222
+ brief = dict(content)
223
+ brief.update({"brief_id": brief_id, "version": 1,
224
+ "status": "draft", "approval": None})
225
+ brief["hash"] = brief_hash(brief)
226
+ state["briefs"][brief_id] = brief
227
+ receipt = emit_receipt(state, "brief_created", {
228
+ "brief_id": brief_id, "version": 1, "hash": brief["hash"],
229
+ "category": brief["category"],
230
+ "ceiling_all_in": money(brief["ceiling_all_in"])})
231
+ return {"brief": public_brief(brief), "receipt_seq": receipt["seq"]}
232
+
233
+
234
+ def t_brief_update(state, args):
235
+ brief_id = str(args.get("brief_id") or "")
236
+ brief = state["briefs"].get(brief_id)
237
+ if not brief:
238
+ raise ToolError(f"unknown brief: {brief_id}")
239
+ changes = args.get("changes") or {}
240
+ unknown = set(changes) - set(BRIEF_CONTENT_FIELDS)
241
+ if unknown:
242
+ raise ToolError(f"not brief content fields: {sorted(unknown)}")
243
+ was_approved = bool(brief.get("approval"))
244
+ old_hash = brief["hash"]
245
+ for key, value in changes.items():
246
+ brief[key] = value
247
+ validate_brief_content(brief_content(brief))
248
+ brief["version"] += 1
249
+ brief["hash"] = brief_hash(brief)
250
+ # Any post-approval edit invalidates the approval for the changed
251
+ # brief (spec 5.1: approval binds hashes, not vibes).
252
+ brief["approval"] = None
253
+ brief["status"] = "draft"
254
+ receipt = emit_receipt(state, "brief_version_created", {
255
+ "brief_id": brief_id, "version": brief["version"],
256
+ "old_hash": old_hash, "new_hash": brief["hash"],
257
+ "changed_fields": sorted(changes),
258
+ "approval_invalidated": was_approved})
259
+ return {"brief": public_brief(brief), "receipt_seq": receipt["seq"],
260
+ "approval_invalidated": was_approved}
261
+
262
+
263
+ def t_brief_approve(state, args):
264
+ brief_id = str(args.get("brief_id") or "")
265
+ brief = state["briefs"].get(brief_id)
266
+ if not brief:
267
+ raise ToolError(f"unknown brief: {brief_id}")
268
+ claimed = str(args.get("brief_hash") or "")
269
+ if claimed != brief["hash"]:
270
+ raise ToolError(
271
+ "approval binds the exact brief hash: supplied "
272
+ f"{claimed or '(none)'} != current {brief['hash']}")
273
+ approval = {
274
+ "approver": str(args.get("approver") or "owner"),
275
+ "approved_at": str(args.get("approved_at") or now_iso()),
276
+ "brief_hash": brief["hash"],
277
+ "variant_hashes": args.get("variant_hashes") or {},
278
+ }
279
+ brief["approval"] = approval
280
+ brief["status"] = "approved"
281
+ receipt = emit_receipt(state, "brief_approved", {
282
+ "brief_id": brief_id, "version": brief["version"],
283
+ "brief_hash": brief["hash"], "approver": approval["approver"],
284
+ "variant_hashes": approval["variant_hashes"]})
285
+ return {"brief": public_brief(brief), "receipt_seq": receipt["seq"]}
286
+
287
+
288
+ def t_brief_read(state, args):
289
+ brief = state["briefs"].get(str(args.get("brief_id") or ""))
290
+ if not brief:
291
+ raise ToolError(f"unknown brief: {args.get('brief_id')}")
292
+ return {"brief": public_brief(brief)}
293
+
294
+
295
+ # --------------------------------------------------------------------- quotes
296
+ def normalize_quote(raw: dict) -> dict:
297
+ """Normalize any platform model to one quote record (spec 5.3).
298
+
299
+ Face price and itemized fees stay separate forever. The ALL-IN
300
+ total counts the face plus buyer-borne fees plus platform-stated
301
+ tax. Pro-borne fees are itemized (they explain quote levels) but
302
+ are not charged to the buyer. If the platform does not expose its
303
+ fees, the quote is UNEXPOSED and carries no all-in total at all.
304
+ """
305
+ model = str(raw.get("model") or "")
306
+ if model not in QUOTE_MODELS:
307
+ raise ToolError(f"unknown quote model: {model or '(none)'}; "
308
+ f"expected one of {sorted(QUOTE_MODELS)}")
309
+ pro = raw.get("pro") or {}
310
+ if not pro.get("platform_id"):
311
+ raise ToolError("quote requires pro.platform_id")
312
+ hourly = None
313
+ if model == "hourly":
314
+ rate = raw.get("hourly_rate")
315
+ cap = raw.get("hour_cap")
316
+ if rate is None or cap is None:
317
+ raise ToolError("hourly quotes require hourly_rate and the "
318
+ "owner-approved hour_cap (spec 5.3)")
319
+ face = money(float(rate) * float(cap))
320
+ hourly = {"rate": money(rate), "hour_cap": float(cap),
321
+ "basis": "rate x owner-approved hour cap; overruns "
322
+ "return to review, never auto-extend"}
323
+ else:
324
+ if raw.get("job_price") is None:
325
+ raise ToolError(f"model {model} requires job_price")
326
+ face = money(raw["job_price"])
327
+ fees = []
328
+ for fee in raw.get("fees") or []:
329
+ bearer = str(fee.get("bearer") or "")
330
+ if bearer not in ("buyer", "pro", "split"):
331
+ raise ToolError("each fee requires bearer: buyer|pro|split")
332
+ fees.append({"name": str(fee.get("name") or "fee"),
333
+ "amount": money(fee["amount"]), "bearer": bearer})
334
+ bearers = {f["bearer"] for f in fees}
335
+ fee_bearer = ("split" if len(bearers) > 1
336
+ else (bearers.pop() if bearers else "none"))
337
+ fees_exposed = bool(raw.get("fees_exposed", False))
338
+ tax = money(raw.get("tax") or 0)
339
+ buyer_fees = money(sum(f["amount"] for f in fees
340
+ if f["bearer"] in ("buyer", "split")))
341
+ exception = raw.get("owner_fee_exception") or None
342
+ if fees_exposed:
343
+ all_in = money(face + buyer_fees + tax)
344
+ fees_status = "EXPOSED"
345
+ elif exception and fees:
346
+ # Written per-quote owner exception (spec 5.3): the platform
347
+ # does not expose its fees, but the owner supplied the figures
348
+ # (e.g. confirmed on the platform's own hire screen) in the
349
+ # approval pack. The status stays UNEXPOSED — the platform
350
+ # never exposed them — and the all-in is labeled as computed
351
+ # from the owner-supplied itemization under exception.
352
+ all_in = money(face + buyer_fees + tax)
353
+ fees_status = "UNEXPOSED"
354
+ else:
355
+ # No silent incompletes: an unexposed-fee quote with nothing
356
+ # to total has no all-in and cannot be hired under a ceiling.
357
+ all_in = None
358
+ fees_status = "UNEXPOSED"
359
+ quote = {
360
+ "platform": str(raw.get("platform") or ""),
361
+ "pro": {
362
+ "platform_id": str(pro["platform_id"]),
363
+ "display_name": pro.get("display_name"),
364
+ "rating": pro.get("rating"),
365
+ "hires": pro.get("hires"),
366
+ # License claims are surfaced verbatim with provenance;
367
+ # the core never asserts a claim is valid (spec 8.3).
368
+ "license_claims": pro.get("license_claims") or [],
369
+ "badges": pro.get("badges") or [],
370
+ },
371
+ "model": model,
372
+ "job_price_face": face,
373
+ "hourly": hourly,
374
+ "fees": fees,
375
+ "fee_bearer": fee_bearer,
376
+ "buyer_fees_total": buyer_fees,
377
+ "tax": tax,
378
+ "tip": raw.get("tip") or {"mode": "n/a"},
379
+ "all_in_total": all_in,
380
+ "fees_status": fees_status,
381
+ "owner_fee_exception": exception,
382
+ "currency": str(raw.get("currency") or "USD"),
383
+ "valid_until": raw.get("valid_until"),
384
+ "raw_quote_ref": raw.get("raw_quote_ref"),
385
+ }
386
+ quote["quote_id"] = "quote_" + sha(canon(quote))[:12]
387
+ return quote
388
+
389
+
390
+ def ceiling_test(quote: dict, ceiling) -> dict:
391
+ ceiling = money(ceiling)
392
+ if quote["fees_status"] == "UNEXPOSED" and \
393
+ not quote.get("owner_fee_exception"):
394
+ return {"ceiling_all_in": ceiling, "hireable_under_ceiling": False,
395
+ "reason": "fees UNEXPOSED: a quote whose fees the "
396
+ "platform does not expose cannot be hired "
397
+ "under a ceiling (spec 5.3); only a written "
398
+ "per-quote owner exception in the approval "
399
+ "pack can lift this"}
400
+ if quote["all_in_total"] is None:
401
+ return {"ceiling_all_in": ceiling, "hireable_under_ceiling": False,
402
+ "reason": "no all-in total could be computed (fees "
403
+ "UNEXPOSED and no owner-supplied itemization "
404
+ "under a written exception)"}
405
+ ok = quote["all_in_total"] <= ceiling
406
+ reason = ("all-in total is within the ceiling"
407
+ if ok else "all-in total exceeds the ceiling")
408
+ if quote["fees_status"] == "UNEXPOSED":
409
+ reason += " (owner fee exception on record)"
410
+ return {"ceiling_all_in": ceiling, "hireable_under_ceiling": ok,
411
+ "reason": reason}
412
+
413
+
414
+ def t_quote_normalize(state, args):
415
+ quote = normalize_quote(args)
416
+ if quote["quote_id"] not in state["quotes"]:
417
+ state["quotes"][quote["quote_id"]] = quote
418
+ emit_receipt(state, "quote_normalized", {
419
+ "quote_id": quote["quote_id"], "platform": quote["platform"],
420
+ "pro_id": quote["pro"]["platform_id"], "model": quote["model"],
421
+ "all_in_total": quote["all_in_total"],
422
+ "fees_status": quote["fees_status"]})
423
+ out = {"quote": quote}
424
+ if args.get("ceiling_all_in") is not None:
425
+ out["ceiling_test"] = ceiling_test(quote, args["ceiling_all_in"])
426
+ return out
427
+
428
+
429
+ def t_quote_read(state, args):
430
+ quote = state["quotes"].get(str(args.get("quote_id") or ""))
431
+ if not quote:
432
+ raise ToolError(f"unknown quote: {args.get('quote_id')}")
433
+ out = {"quote": quote}
434
+ if args.get("ceiling_all_in") is not None:
435
+ out["ceiling_test"] = ceiling_test(quote, args["ceiling_all_in"])
436
+ return out
437
+
438
+
439
+ # ------------------------------------------------------------------ hire gate
440
+ def expected_quote_token(quote_id: str) -> str:
441
+ return f"approve-quote-{quote_id}"
442
+
443
+
444
+ def expected_person_token(quote_id: str) -> str:
445
+ return f"confirm-person-{quote_id}"
446
+
447
+
448
+ def t_hire_check(state, args):
449
+ """The hire gate (spec 6 + 7.4): pure mandate evaluation.
450
+
451
+ Executes nothing. Returns a decision and whether the quote is
452
+ hireable RIGHT NOW. In v1 the selection of a person requires an
453
+ owner-confirm token even when every mandate check passes.
454
+ """
455
+ brief = state["briefs"].get(str(args.get("brief_id") or ""))
456
+ quote = state["quotes"].get(str(args.get("quote_id") or ""))
457
+ as_of = parse_time(str(args.get("as_of") or now_iso()))
458
+ reasons, notes = [], []
459
+
460
+ if not brief:
461
+ reasons.append(f"unknown brief: {args.get('brief_id')}")
462
+ if not quote:
463
+ reasons.append(f"unknown quote: {args.get('quote_id')}")
464
+
465
+ mandate = args.get("mandate")
466
+ approval_ok = False
467
+ person_ok = False
468
+ if brief and quote:
469
+ approval = brief.get("approval")
470
+ if not approval or approval.get("brief_hash") != brief["hash"]:
471
+ reasons.append("brief is not approved at its current hash "
472
+ "(approval binds hashes; spec 5.1)")
473
+ if not mandate:
474
+ reasons.append("NO_MANDATE: no mandate, no hire (spec 6.1)")
475
+ else:
476
+ expires = parse_time(mandate.get("expires_at"))
477
+ if expires is None or expires <= as_of:
478
+ reasons.append("mandate expired or has no expiry "
479
+ "(spec 6.1)")
480
+ if quote["platform"] not in \
481
+ (mandate.get("platforms") or []):
482
+ reasons.append(
483
+ f"platform {quote['platform']!r} not in mandate "
484
+ "platform allowlist")
485
+ if brief["category"] not in \
486
+ (mandate.get("categories") or []):
487
+ reasons.append(
488
+ f"category {brief['category']!r} not in mandate "
489
+ "category allowlist")
490
+ cet = ceiling_test(quote, mandate.get("cap", 0)) \
491
+ if mandate.get("cap") is not None else None
492
+ brief_cet = ceiling_test(quote, brief["ceiling_all_in"])
493
+ if cet and not cet["hireable_under_ceiling"]:
494
+ reasons.append("mandate cap: " + cet["reason"])
495
+ if not brief_cet["hireable_under_ceiling"]:
496
+ reasons.append("brief ceiling: " + brief_cet["reason"])
497
+ threshold = mandate.get("approval_threshold")
498
+ if threshold is not None and quote["all_in_total"] is not None \
499
+ and quote["all_in_total"] > float(threshold):
500
+ if args.get("quote_approval_token") == \
501
+ expected_quote_token(quote["quote_id"]):
502
+ approval_ok = True
503
+ notes.append("above-threshold quote approved by "
504
+ "owner token for THIS quote (spec 6.2)")
505
+ else:
506
+ reasons.append(
507
+ "all-in total exceeds the mandate approval "
508
+ "threshold; an owner approval token for this "
509
+ "exact quote is required (spec 6.2)")
510
+ valid_until = parse_time(quote.get("valid_until"))
511
+ if valid_until is not None and valid_until <= as_of:
512
+ reasons.append("quote has expired (valid_until passed)")
513
+ # Person-selection rule (spec 7.4): v1 default is
514
+ # owner-confirmed, mandate or not.
515
+ if args.get("person_confirm_token") == \
516
+ expected_person_token(quote["quote_id"]):
517
+ person_ok = True
518
+ else:
519
+ notes.append("PERSON_CONFIRM_REQUIRED: in v1 the selection "
520
+ "of a person is owner-confirmed even under a "
521
+ "mandate (spec 7.4)")
522
+
523
+ if reasons:
524
+ decision, hireable = "DENY", False
525
+ elif not person_ok:
526
+ decision, hireable = "MANDATE_ALLOW_PERSON_PENDING", False
527
+ else:
528
+ decision, hireable = "ALLOW", True
529
+ detail = {"brief_id": args.get("brief_id"),
530
+ "quote_id": args.get("quote_id"), "decision": decision,
531
+ "hireable": hireable, "reasons": reasons, "notes": notes,
532
+ "mandate_id": (mandate or {}).get("mandate_id"),
533
+ "executes": "nothing — evaluation only (G0)"}
534
+ emit_receipt(state, "hire_check", detail)
535
+ return detail
536
+
537
+
538
+ # ------------------------------------------------------- completion + reviews
539
+ def t_completion_verify(state, args):
540
+ brief = state["briefs"].get(str(args.get("brief_id") or ""))
541
+ if not brief:
542
+ raise ToolError(f"unknown brief: {args.get('brief_id')}")
543
+ scope_items = (brief.get("scope") or {}).get("items") or []
544
+ results = args.get("scope_results") or []
545
+ by_item = {r.get("item"): r for r in results}
546
+ checked = []
547
+ for item in scope_items:
548
+ result = by_item.get(item)
549
+ if result is None:
550
+ checked.append({"item": item, "status": "unattested",
551
+ "attestation": None})
552
+ else:
553
+ status = str(result.get("status") or "")
554
+ if status not in ("done", "partial", "not_done"):
555
+ raise ToolError("scope result status must be done | "
556
+ "partial | not_done")
557
+ checked.append({"item": item, "status": status,
558
+ "attestation": result.get("attestation")})
559
+ platform_state = str(args.get("platform_state") or "unknown")
560
+ incomplete = [c for c in checked if c["status"] != "done"]
561
+ # Platform state and owner verification are separate fields and
562
+ # are never merged (spec 7.2). A platform that claims completion
563
+ # the owner does not attest opens an exception, not an edit.
564
+ mismatch = bool(incomplete) and \
565
+ platform_state.strip().lower() in ("completed", "complete",
566
+ "done", "paid", "released")
567
+ if mismatch:
568
+ verification_status = "exception"
569
+ elif incomplete:
570
+ verification_status = "partial"
571
+ else:
572
+ verification_status = "owner_verified"
573
+ record = {
574
+ "brief_id": brief["brief_id"],
575
+ "brief_hash": brief["hash"],
576
+ "quote_id": args.get("quote_id"),
577
+ "platform": args.get("platform"),
578
+ "pro_display_name": args.get("pro_display_name"),
579
+ "platform_state": platform_state,
580
+ "platform_state_basis": "read back verbatim; never smoothed "
581
+ "(spec 7.2)",
582
+ "scope_check": checked,
583
+ "verification_status": verification_status,
584
+ "platform_owner_mismatch": mismatch,
585
+ "timeliness": args.get("timeliness"),
586
+ "communication": args.get("communication"),
587
+ "quality_notes": args.get("quality_notes"),
588
+ "verified_at": str(args.get("verified_at") or now_iso()),
589
+ }
590
+ record["verification_id"] = \
591
+ "verify_" + sha(canon(record))[:12]
592
+ state["verifications"][record["verification_id"]] = record
593
+ receipt = emit_receipt(state, "completion_verified", {
594
+ "verification_id": record["verification_id"],
595
+ "brief_id": brief["brief_id"],
596
+ "verification_status": verification_status,
597
+ "platform_owner_mismatch": mismatch})
598
+ return {"verification": record, "receipt_seq": receipt["seq"]}
599
+
600
+
601
+ def t_review_draft(state, args):
602
+ record = state["verifications"].get(
603
+ str(args.get("verification_id") or ""))
604
+ if not record:
605
+ raise ToolError(
606
+ f"unknown verification: {args.get('verification_id')}")
607
+ notes_blob = " ".join(str(record.get(k) or "") for k in
608
+ ("quality_notes", "timeliness",
609
+ "communication")).lower()
610
+ for pattern in LEVERAGE_PATTERNS:
611
+ if pattern in notes_blob:
612
+ raise ToolError(
613
+ "prohibited leverage language in the verification "
614
+ f"record ({pattern!r}); review drafts are factual and "
615
+ "record-bound only (spec 7.3)")
616
+ brief = state["briefs"].get(record["brief_id"]) or {}
617
+ done = sum(1 for c in record["scope_check"] if c["status"] == "done")
618
+ total = len(record["scope_check"])
619
+ lines = [
620
+ f"Hired for {brief.get('category', 'a task')} "
621
+ f"({brief.get('service', 'service')}) via "
622
+ f"{record.get('platform') or 'the platform'}.",
623
+ f"Completion verified against the brief: {done} of {total} "
624
+ "scope items done.",
625
+ ]
626
+ if record["verification_status"] != "owner_verified":
627
+ lines.append(
628
+ f"Verification status: {record['verification_status']}.")
629
+ if record.get("timeliness"):
630
+ lines.append(f"Timeliness: {record['timeliness']}.")
631
+ if record.get("communication"):
632
+ lines.append(f"Communication: {record['communication']}.")
633
+ # Sentiment enters ONLY from the record's own quality notes.
634
+ if record.get("quality_notes"):
635
+ lines.append(f"Quality: {record['quality_notes']}.")
636
+ text = " ".join(lines)
637
+ draft = {
638
+ "verification_id": record["verification_id"],
639
+ "brief_id": record["brief_id"],
640
+ "pro_display_name": record.get("pro_display_name"),
641
+ "text": text,
642
+ "basis": "verification record only (spec 7.3)",
643
+ "status": "DRAFT — owner approves the exact text before any "
644
+ "submission; drafts never submit themselves",
645
+ }
646
+ emit_receipt(state, "review_drafted", {
647
+ "verification_id": record["verification_id"],
648
+ "draft_hash": sha(text)})
649
+ return {"review_draft": draft}
650
+
651
+
652
+ def t_receipt_list(state, args):
653
+ limit = args.get("limit")
654
+ receipts = state["receipts"]
655
+ if limit is not None:
656
+ receipts = receipts[-int(limit):]
657
+ return {"receipts": receipts, "count": len(state["receipts"]),
658
+ "chain_valid": verify_receipt_chain(state["receipts"])}
659
+
660
+
661
+ TOOLS = {
662
+ "brief_create": (
663
+ t_brief_create,
664
+ "Create a canonical Task Brief: category, service, scope "
665
+ "{statement, items[]}, ZIP-tier location, timing window, "
666
+ "ALL-IN ceiling, tip policy, photos (EXIF-strip status "
667
+ "recorded per photo), license_required (unknown|no|yes:<type>), "
668
+ "safety/access block, intended/prohibited platforms, and a "
669
+ "details bag adapters draw native form answers from. Issues "
670
+ "the version-1 hash an approval binds to.",
671
+ {"brief_id": {"type": "string"}, "category": {"type": "string"},
672
+ "service": {"type": "string"}, "scope": {"type": "object"},
673
+ "location": {"type": "object"}, "timing": {"type": "object"},
674
+ "ceiling_all_in": {"type": "number"},
675
+ "tip_policy": {"type": "object"},
676
+ "photos": {"type": "array", "items": {"type": "object"}},
677
+ "license_required": {"type": "string"},
678
+ "safety_access": {"type": "object"},
679
+ "intended_platforms": {"type": "array",
680
+ "items": {"type": "string"}},
681
+ "prohibited_platforms": {"type": "array",
682
+ "items": {"type": "string"}},
683
+ "details": {"type": "object"}},
684
+ ["category", "service", "scope", "location", "timing",
685
+ "ceiling_all_in", "license_required"]),
686
+ "brief_update": (
687
+ t_brief_update,
688
+ "Create a new brief version from content-field changes. Any "
689
+ "post-approval edit invalidates the approval and returns the "
690
+ "brief to draft (approval binds hashes, spec 5.1).",
691
+ {"brief_id": {"type": "string"}, "changes": {"type": "object"}},
692
+ ["brief_id", "changes"]),
693
+ "brief_approve": (
694
+ t_brief_approve,
695
+ "Record the owner's approval of the EXACT current brief hash, "
696
+ "plus the per-platform variant hashes approved with it. A "
697
+ "hash mismatch is refused.",
698
+ {"brief_id": {"type": "string"}, "brief_hash": {"type": "string"},
699
+ "approver": {"type": "string"},
700
+ "approved_at": {"type": "string"},
701
+ "variant_hashes": {"type": "object"}},
702
+ ["brief_id", "brief_hash"]),
703
+ "brief_read": (
704
+ t_brief_read,
705
+ "Read a brief with its version, hash, and approval validity.",
706
+ {"brief_id": {"type": "string"}}, ["brief_id"]),
707
+ "quote_normalize": (
708
+ t_quote_normalize,
709
+ "Normalize a quote from any platform model (lead_model, "
710
+ "stated_budget, fixed_price, hourly) into one record: face "
711
+ "price, itemized fees with bearers, tax, and ALL-IN total "
712
+ "kept separate forever. Fees the platform does not expose "
713
+ "mark the quote UNEXPOSED with no all-in. Optional "
714
+ "ceiling_all_in runs the ceiling test.",
715
+ {"platform": {"type": "string"}, "pro": {"type": "object"},
716
+ "model": {"type": "string"}, "job_price": {"type": "number"},
717
+ "hourly_rate": {"type": "number"},
718
+ "hour_cap": {"type": "number"},
719
+ "fees": {"type": "array", "items": {"type": "object"}},
720
+ "fees_exposed": {"type": "boolean"},
721
+ "tax": {"type": "number"}, "tip": {"type": "object"},
722
+ "currency": {"type": "string"},
723
+ "valid_until": {"type": "string"},
724
+ "raw_quote_ref": {"type": "string"},
725
+ "owner_fee_exception": {"type": "object"},
726
+ "ceiling_all_in": {"type": "number"}},
727
+ ["platform", "pro", "model"]),
728
+ "quote_read": (
729
+ t_quote_read,
730
+ "Read a normalized quote; optional ceiling_all_in re-runs the "
731
+ "ceiling test.",
732
+ {"quote_id": {"type": "string"},
733
+ "ceiling_all_in": {"type": "number"}}, ["quote_id"]),
734
+ "hire_check": (
735
+ t_hire_check,
736
+ "Evaluate a hire against a live Procure/Agent Wallet mandate: "
737
+ "approved brief hash, mandate presence + expiry, cap vs "
738
+ "all-in, brief ceiling, category + platform allowlists, "
739
+ "approval threshold (above = owner token for THIS quote), "
740
+ "quote expiry, fee exposure. v1 person-selection always "
741
+ "needs an owner-confirm token. Returns ALLOW / "
742
+ "MANDATE_ALLOW_PERSON_PENDING / DENY with named reasons. "
743
+ "Executes nothing; receipted either way.",
744
+ {"brief_id": {"type": "string"}, "quote_id": {"type": "string"},
745
+ "mandate": {"type": "object"},
746
+ "quote_approval_token": {"type": "string"},
747
+ "person_confirm_token": {"type": "string"},
748
+ "as_of": {"type": "string"}}, ["brief_id", "quote_id"]),
749
+ "completion_verify": (
750
+ t_completion_verify,
751
+ "Verify completion against the brief's scope statement: each "
752
+ "scope item done/partial/not_done with owner attestation. "
753
+ "Platform state is stored verbatim and separately; a "
754
+ "platform-completed claim the owner does not attest opens "
755
+ "an exception record (spec 7.2).",
756
+ {"brief_id": {"type": "string"}, "quote_id": {"type": "string"},
757
+ "platform": {"type": "string"},
758
+ "pro_display_name": {"type": "string"},
759
+ "platform_state": {"type": "string"},
760
+ "scope_results": {"type": "array",
761
+ "items": {"type": "object"}},
762
+ "timeliness": {"type": "string"},
763
+ "communication": {"type": "string"},
764
+ "quality_notes": {"type": "string"},
765
+ "verified_at": {"type": "string"}}, ["brief_id"]),
766
+ "review_draft": (
767
+ t_review_draft,
768
+ "Draft review text from a verification record ONLY: what was "
769
+ "hired, what was verified, and timeliness/communication/"
770
+ "quality strictly as recorded. No sentiment the record does "
771
+ "not carry; leverage language is refused at draft time. "
772
+ "The draft never submits itself.",
773
+ {"verification_id": {"type": "string"}}, ["verification_id"]),
774
+ "receipt_list": (
775
+ t_receipt_list,
776
+ "List the append-only, hash-chained gig-core receipts and "
777
+ "verify the chain.",
778
+ {"limit": {"type": "integer"}}, []),
779
+ }
780
+
781
+ # Capability denylist, stated in code: G0 evaluates and records only.
782
+ # Live submission, live hiring, payment movement, and review
783
+ # submission are adapter/G1 surfaces behind owner gates — and
784
+ # session/scraping routes are prohibited at any tier (spec 8.6).
785
+ FORBIDDEN_TOOL_NAMES = {
786
+ "request_submit", "hire_execute", "payment_execute",
787
+ "review_submit", "pro_contact_export", "session_login",
788
+ "cookie_reuse", "scrape_quotes", "scrape_profiles",
789
+ }
790
+
791
+
792
+ def tool_list():
793
+ return [{"name": name, "description": description,
794
+ "inputSchema": {"type": "object", "properties": props,
795
+ "required": required}}
796
+ for name, (_, description, props, required) in TOOLS.items()]
797
+
798
+
799
+ def handle(req: dict):
800
+ method, rid = req.get("method", ""), req.get("id")
801
+ if method == "initialize":
802
+ return {"jsonrpc": "2.0", "id": rid, "result": {
803
+ "protocolVersion": req.get("params", {}).get(
804
+ "protocolVersion", "2025-03-26"),
805
+ "capabilities": {"tools": {}},
806
+ "serverInfo": {"name": "eclipse-gig-core", "version": "0.1.0"}}}
807
+ if method == "ping":
808
+ return {"jsonrpc": "2.0", "id": rid, "result": {}}
809
+ if method == "tools/list":
810
+ return {"jsonrpc": "2.0", "id": rid,
811
+ "result": {"tools": tool_list()}}
812
+ if method == "tools/call":
813
+ name = req["params"]["name"]
814
+ args = req["params"].get("arguments", {})
815
+ if name in FORBIDDEN_TOOL_NAMES or name not in TOOLS:
816
+ return {"jsonrpc": "2.0", "id": rid, "error": {
817
+ "code": -32602,
818
+ "message": f"unknown or G0-forbidden tool {name}"}}
819
+ state = load_state()
820
+ try:
821
+ out = TOOLS[name][0](state, args)
822
+ save_state(state)
823
+ return {"jsonrpc": "2.0", "id": rid, "result": {
824
+ "content": [{"type": "text",
825
+ "text": json.dumps(out, indent=1)}]}}
826
+ except ToolError as exc:
827
+ return {"jsonrpc": "2.0", "id": rid, "result": {
828
+ "content": [{"type": "text",
829
+ "text": json.dumps({"error": str(exc)})}],
830
+ "isError": True}}
831
+ if rid is not None:
832
+ return {"jsonrpc": "2.0", "id": rid, "error": {
833
+ "code": -32601, "message": f"method not found: {method}"}}
834
+ return None
835
+
836
+
837
+ def main():
838
+ for line in sys.stdin:
839
+ line = line.strip()
840
+ if not line:
841
+ continue
842
+ try:
843
+ response = handle(json.loads(line))
844
+ except Exception as exc: # never crash the transport
845
+ response = {"jsonrpc": "2.0", "id": None, "error": {
846
+ "code": -32603, "message": str(exc)}}
847
+ if response is not None:
848
+ sys.stdout.write(json.dumps(response) + "\n")
849
+ sys.stdout.flush()
850
+
851
+
852
+ if __name__ == "__main__":
853
+ main()
854
+
855
+ # Eclipse Gig Core MCP · © 2026 Cyan Eclipse Technologies LLC · cetech.ai