Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.
goal
Rewrite ONE unique forge module as entirely new, behavior-preserving, installable,
control-config'd Python, per the FORGE-wide refactor standard #200 (#std).
This ticket covers ONE module — the one identified below — and nothing else. It was filed
against the module's DEDUPLICATED identity, so if that module was copy-pasted across the
forge, every copy is covered by this single ticket.
MODULE KEY : forge-rw:6c779b342584ea39
(the ticketer's idempotency marker — it lives in this ticket's TITLE. Do
not edit the title, or the module will be re-filed as a duplicate.)
IDENTITY : AST-NORMALIZED — codebean's `_ast_norm` (#183), i.e.
ast.dump(ast.parse(ast.unparse(ast.parse(src)))).
Copies of this module that differ only in comments, blank lines,
quoting or spacing carry the SAME module-sha and are covered by THIS
ticket. A differing docstring is real content, so a module whose
docstring differs is a DIFFERENT module with its own ticket.
SIZE : 10103 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : agent_tools_suite
path : mcplib.py
ref : HEAD (HEAD)
blob : c3796f354363df36faeb4f7a35de2f790d81166b (git blob sha1)
REFERENCE ONLY — you do NOT need this command. This module's bytes are embedded
COMPLETE in THE ORIGINAL SOURCE block below, and that is what the rewrite is based
on. For a reader who does have forge access:
git --git-dir=/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes/agent_tools_suite.git cat-file blob c3796f354363df36faeb4f7a35de2f790d81166b
THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS: none — this module is unique to
agent_tools_suite among the repos scanned so far.
HISTORICAL OCCURRENCES of this exact blob — every OTHER DISTINCT {repo, path}
the omni-git index (`occ`, joined on the git blob sha1) records this
byte-identical file at, with the index's own `ts` for the first row of each.
Any place this run enumerated LIVE is listed under the forge tips above, not
here. Context for the rewrite, not extra work:
testmonkeyalpha-group/jazz-forge/agent_tools_suite:mcplib.py [occ.ts gitlab]
THE ORIGINAL SOURCE, EMBEDDED — YOU NEED NO FORGE ACCESS TO DO THIS TICKET.
The module's own bytes are reproduced below, inside this ticket. THEY are the
authoritative copy and the behavior-preserving basis: rewrite from them. The
`git --git-dir=... cat-file blob` command under CANONICAL SOURCE above is kept only as a
REFERENCE for a reader who happens to have forge access — running it is NOT part of this
ticket. (Why the bytes are here at all: the pair_loop worker that executes this ticket is
sandboxed to the ticket's own congruency worktree and cannot read the forge's bare repos.
Ticket #346 is the fix that put them on the ticket.)
The block BEGINS at the line
===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)===
and ENDS at the first line
===END SOURCE===
Everything strictly between those two lines is the module — 10103 byte(s), byte for byte, with no re-indentation, no re-wrapping and nothing
elided.
===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)===
#!/usr/bin/env python3
"""mcplib — zero-dependency MCP stdio helpers, shared by the gate, the coupler, and
the leaf servers.
MCP stdio is newline-delimited JSON-RPC 2.0. Pure stdlib, so nothing can rot:
- Server: the host-facing side (initialize / tools/list / tools/call loop).
- Downstream: the client side (spawn a child MCP server, handshake, route calls).
Everything is synchronous and sequential — requests over stdio arrive one at a time,
which keeps this small and auditable.
"""
import json
import os
import subprocess
import sys
DEFAULT_PROTOCOL = "2024-11-05"
def text_result(text, is_error=False):
"""A standard MCP text tool-result."""
return {"content": [{"type": "text", "text": text}], "isError": is_error}
def load_registry(here):
"""Read the registry beside `here`. `ATS_REGISTRY` overrides the filename (per-agent scoping —
e.g. registry.crankfix.json for a crankfix-only launch); it's inherited by the coupler the gate
spawns, so one env var scopes the whole stack. A tool that cannot see its registry throws."""
name = os.environ.get("ATS_REGISTRY", "registry.json")
path = name if os.path.isabs(name) else os.path.join(here, name)
if not os.path.isfile(path):
raise SystemExit("registry not found: %s" % path)
with open(path) as f:
return json.load(f)
# --------------------------------------------------------------------------- #
# Host-facing server #
# --------------------------------------------------------------------------- #
class Server:
"""Drives the host-facing MCP stdio loop.
list_tools() -> [ {name, description, inputSchema}, ... ]
call_tool(name, arguments) -> result dict (use text_result()).
"""
def __init__(self, name, version, list_tools, call_tool, watch_tool_changes=False):
self.info = {"name": name, "version": version}
self._list_tools = list_tools
self._call_tool = call_tool
# When True, advertise the tools.listChanged capability and push a
# notifications/tools/list_changed whenever the tool list changes after a
# tools/call — so the host re-fetches without a manual reconnect.
self._watch = watch_tool_changes
self._last_tool_names = None
def _send(self, msg):
sys.stdout.write(json.dumps(msg) + "\n")
sys.stdout.flush()
def _reply(self, mid, result):
self._send({"jsonrpc": "2.0", "id": mid, "result": result})
def _error(self, mid, code, message):
self._send({"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}})
def _notify(self, method, params=None):
"""A server-initiated notification (no id — the host does not reply)."""
self._send({"jsonrpc": "2.0", "method": method, "params": params or {}})
def _maybe_notify_tools_changed(self):
"""If the tool list changed since it was last observed, tell the host to
re-fetch it (MCP notifications/tools/list_changed)."""
try:
names = [t.get("name") for t in self._list_tools()]
except Exception:
return
if self._last_tool_names is not None and names != self._last_tool_names:
self._notify("notifications/tools/list_changed")
self._last_tool_names = names
def _handle(self, msg):
mid = msg.get("id")
method = msg.get("method")
params = msg.get("params") or {}
is_request = mid is not None
if method == "initialize":
caps = {"tools": {"listChanged": True} if self._watch else {}}
self._reply(mid, {
"protocolVersion": params.get("protocolVersion", DEFAULT_PROTOCOL),
"capabilities": caps,
"serverInfo": self.info,
})
elif method in ("notifications/initialized", "initialized"):
return
elif method == "ping":
if is_request:
self._reply(mid, {})
elif method == "tools/list":
tools = self._list_tools()
self._reply(mid, {"tools": tools})
if self._watch:
self._last_tool_names = [t.get("name") for t in tools]
elif method == "tools/call":
name = params.get("name")
try:
result = self._call_tool(name, params.get("arguments") or {})
except Exception as e: # never let one bad call kill the server
result = text_result('tool "%s" threw: %s' % (name, e), True)
self._reply(mid, result)
if self._watch: # a call may have changed the tool list
self._maybe_notify_tools_changed()
else:
if is_request:
self._error(mid, -32601, "Method not found: %s" % method)
def serve(self):
sys.stderr.write("[%s] up\n" % self.info["name"])
# readline() (not `for line in sys.stdin`) — the latter read-ahead-buffers on a
# pipe and would stall responses until the buffer fills or stdin closes.
while True:
line = sys.stdin.readline()
if line == "": # EOF: host closed stdin → shut down
break
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except Exception:
continue
self._handle(msg)
# --------------------------------------------------------------------------- #
# Downstream client #
# --------------------------------------------------------------------------- #
class Downstream:
"""A child MCP server this process spawns and talks to as a client.
`spec` = {command, args?, env?}. A command/arg/env-value written as an explicit
relative path (./x or ../x) is resolved against `base_dir`, so servers that ship
together never need an absolute path. Bare relative args still work because the
child is spawned with cwd=base_dir.
"""
def __init__(self, name, spec, base_dir):
self.name = name
self.spec = spec or {}
self.base_dir = base_dir
self.proc = None
self.tools = []
self.dead = False
self.error = None
self._id = 0
def _resolve(self, s):
if isinstance(s, str) and (s.startswith("./") or s.startswith("../")):
return os.path.normpath(os.path.join(self.base_dir, s))
return s
def start(self):
if self.proc is not None or self.dead:
return
command = self.spec.get("command")
if not command:
self.dead = True
self.error = 'missing "command"'
return
command = self._resolve(command)
args = [self._resolve(a) for a in self.spec.get("args", [])]
env = dict(os.environ)
for k, v in (self.spec.get("env") or {}).items():
env[k] = self._resolve(v)
try:
self.proc = subprocess.Popen(
[command] + args,
cwd=self.base_dir,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=sys.stderr,
text=True,
bufsize=1,
env=env,
)
except Exception as e:
self.dead = True
self.error = str(e)
return
try:
self._request("initialize", {
"protocolVersion": DEFAULT_PROTOCOL,
"capabilities": {},
"clientInfo": {"name": "mcplib", "version": "1.0.0"},
})
self._notify("notifications/initialized")
res = self._request("tools/list", {})
self.tools = (res or {}).get("tools", [])
except Exception as e:
self.dead = True
self.error = str(e)
def _request(self, method, params):
if self.dead or self.proc is None:
raise RuntimeError("downstream '%s' not running (%s)" % (self.name, self.error))
self._id += 1
mid = self._id
payload = json.dumps({"jsonrpc": "2.0", "id": mid, "method": method, "params": params}) + "\n"
self.proc.stdin.write(payload)
self.proc.stdin.flush()
while True:
line = self.proc.stdout.readline()
if line == "":
self.dead = True
self.error = self.error or "downstream closed"
raise RuntimeError("downstream '%s' closed" % self.name)
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except Exception:
continue
if msg.get("id") == mid:
if msg.get("error"):
raise RuntimeError(msg["error"].get("message", "downstream error"))
return msg.get("result")
# ignore notifications / mismatched ids
def _notify(self, method, params=None):
try:
self.proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": method, "params": params or {}}) + "\n")
self.proc.stdin.flush()
except Exception:
pass
def call_tool(self, name, arguments):
return self._request("tools/call", {"name": name, "arguments": arguments or {}})
def refresh_tools(self):
"""Re-fetch this child's tool list (e.g. after it reconnected its own
children), so an aggregating parent can re-expose the current set."""
if self.dead or self.proc is None:
return
try:
res = self._request("tools/list", {})
self.tools = (res or {}).get("tools", [])
except Exception as e:
self.dead = True
self.error = str(e)
def stop(self):
if self.proc and not self.dead:
try:
self.proc.terminate()
except Exception:
pass
===END SOURCE===
THE CALLABLE SURFACE — every callable in the original, lowered with codebean's
`lower_node` (#183). The rewrite must present THIS surface (same names, same
parameters, same defaults) so callers of the original keep working:
text_result(text, is_error=False)
load_registry(here)
Server.__init__(self, name, version, list_tools, call_tool, watch_tool_changes=False)
Server._send(self, msg)
Server._reply(self, mid, result)
Server._error(self, mid, code, message)
Server._notify(self, method, params=None)
Server._maybe_notify_tools_changed(self)
Server._handle(self, msg)
Server.serve(self)
Downstream.__init__(self, name, spec, base_dir)
Downstream._resolve(self, s)
Downstream.start(self)
Downstream._request(self, method, params)
Downstream._notify(self, method, params=None)
Downstream.call_tool(self, name, arguments)
Downstream.refresh_tools(self)
Downstream.stop(self)
CAPTURED BEHAVIORAL EXEMPLARS — real (input, output) pairs mined from THIS module
with criteria_miner (#200's "verified, not asserted"), running the ORIGINAL as the
oracle. THE REWRITE MUST REPRODUCE EVERY PAIR BELOW, exactly:
text_result(text, is_error=False) — 30 exemplar(s) captured:
text_result('content', 'content') == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 'content'}
text_result('content', 'A standard MCP text tool-result.') == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 'A standard MCP text tool-result.'}
text_result('content', 'type') == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 'type'}
text_result('content', 'isError') == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 'isError'}
text_result('content', 'text') == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 'text'}
text_result('content', 0) == {'content': [{'type': 'text', 'text': 'content'}], 'isError': 0}
... 24 more exemplar(s) for this function — re-mine them with
criteria_miner.mine() on the source above
HONEST LIMIT (criteria_miner's own): these cover the SAMPLED inputs, probed
around the function's own branch constants. They are a strong refactor-drift net,
NOT a proof of total equivalence — so treat them as the FLOOR of this ticket's
equivalence evidence and add the edge cases they miss (see ACCEPTANCE).
THE STANDARD — #200's four requirements, which this ticket exists to enforce. All four are
the standard's own words; none is optional:
1. BEHAVIOR-PRESERVING, VERIFIED NOT ASSERTED — the deliverable is ENTIRELY NEW source
code (rewritten, not copied). It must reproduce the ORIGINAL's behavior, and the proof
is the exemplars above (captured via codebean lower_* #183 + criteria_miner
IO-exemplars, exactly as #200 specifies).
2. A PYTHON MODULE — regardless of the source language. This module's source is `.py`;
the rewrite is Python either way.
3. INSTALLABLE — a proper package: pyproject.toml, `pip install -e .` works, console
entry points where applicable. WRITE ONLY YOUR OWN MODULE FILE at
forge2/<repo>/<module>.py. Do NOT hand-author pyproject.toml or __init__.py: the
harness DERIVES them ADDITIVELY from the modules present (congruency/tools/
forge_package.py, #410 — the analogue of the tag-registry fix #281), so your unit never
conflicts with, or clobbers, a sibling module of the same forge2/<repo>/ package. A
package-level pyproject includes every .py in the directory, so your module installs
and imports as `<repo>.<module>` the moment it lands.
4. CONTROL-CONFIG'D — NO hardcoded paths or params. A JSON/registry config object drives
it (the install.json / registry.json idiom of this repo), and per note-for-claude a
tool that cannot see its config THROWS rather than guessing a default. Add ONLY your
own `forge2.<repo>.<module>` section to the package's registry.json (create the file if
it is absent); NEVER read, edit or drop a sibling module's section — the sections are
disjoint keys and stay additive that way.
ACCEPTANCE (#200's, per rewrite ticket) — this ticket settles when ALL of:
a. the new source reproduces every captured exemplar above, plus the edge cases you add
for what sampling cannot reach (paste the runs as evidence);
b. it presents the callable surface above, so existing callers keep working;
c. `pip install -e .` installs it clean from its own pyproject.toml;
d. it runs off a config object with ZERO hardcoded settings, and throws without one.
Kind is `build` (a pair: coder + adversarial tester). This ticket hangs off #200 by a
`spawned` edge, so a stop ticket aimed at #200 brakes every rewrite in this programme.
Filed mechanically by checkouts/current/congruency/tools/forge_refactor_ticketer.py under ticket #201, from the forge bare repos at
/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes. Its original source is embedded above (#346), so this ticket is complete on its own.