Skip to content

mcp_server 0.9.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 03:36
· 10 commits to main since this release
7ab1067
  • The server is now a Rowan project, Mcp, in Tonel. Breaking: installing needs a stone on a
    Rowan 3 extent (extent0.rowan3.dbf); install.sh refuses one without Rowan. The packages
    (Mcp-Core, Mcp-Tests, Mcp-Auth, Mcp-Auth-Tests, Mcp-Grail, Mcp-Grail-Tests) live under
    src/, the specs and the Core, Auth and optional Grail components under rowan/, and the
    .gs groups and their loaders are gone. Changes are made in the image and written out with the new
    ./write-project.sh. install.sh migrates an image installed from .gs. --grail now loads the
    Grail component, which needs a 4.0 image with both Grail and Rowan — 4.0.0.a5 is the first to
    ship a Rowan extent.

  • 3.7.x without Rowan installs a generated topaz file-out. ./export-gs.sh writes Core +
    Auth as one mcp_server.gs with GemStone's own Rowan exporter (no stone needed), and
    ./install.sh --from-gs <file> files it into a stock extent0.dbf. The file is never committed.
    CI now runs four legs on every pull request: 3.7.5 and 3.7.6 from the file-out on a stock extent,
    and 4.0.0.a5 — moved up from a3 — through Rowan on its extent0.rowan3.dbf, with and without
    Grail. Pushing a v<version> tag on main now publishes a GitHub Release with the file-out
    attached, after testing that exact file on 3.7.5 and 3.7.6 (release.yml).

  • The scripts no longer read a .topazini. Every topaz login passes -i. Breaking for
    anyone who kept settings for these scripts there: they belong in .setenv. A .topazini in the
    checkout made install.sh report a failed load over one that had succeeded. CI now plants a hostile
    one on every leg and fails if any topaz reads it.

  • Forked gems reach the stone by localhost
    (#56). The front end, each worker and a Grail
    test gem pinned their own host to localhost but took the stone's from the machine's host name
    whenever the launching login named none — as run-server.sh's does. Where that name did not
    resolve, or resolved to a LAN address, no worker could log in. A host the login names is still
    used. New suite McpWorkerLoginTest.

  • find_python_senders takes every position from Grail
    (#58). Calls and references are placed by
    BaseException pythonSendSitesIn:selector:
    (GemTalk/Grail#1137), which answers each send
    from the VM's send table with its Python position, the same way on both codegen paths. The
    readers that parsed ___curPos___ stores and ___GRAILPOS___ trailers out of generated
    Smalltalk are gone, as is the IR path's line counting. A builtin's probe twin is still one hit,
    now keyed by its PEP 657 span.

  • compile_method ends the stored source with a newline
    (#64). A client almost never sends one, and
    every other way a method reaches the image stores one, so ./write-project.sh wrote a method
    compiled through the tool without the blank line before its closing ]. Only a missing final
    newline is added; a source that already ends in one is stored as sent.

  • The session-lifetime countdown says minutes
    (#47). The time left on a deadline in the
    uncommitted-work warning was phrased like a configured interval, in minutes only for an exact
    multiple of one, so it nearly always read like "1979 seconds left". It now rounds down to whole
    minutes from one minute up ("32 minutes left") and says seconds below that, so it never promises
    more time than is left. The inactivity bound is phrased as before.

  • Every JSON parse failure is now a -32700. McpBase class>>parseBody: caught only Error,
    and not everything the kernel parser can signal is one. It now catches the kernel's Exception,
    looked up through Globals because a Grail image binds #Exception to Python's class first.

  • A request body ending in whitespace is no longer a parse error on GemStone 4.0.0.a3. a3's
    JsonParser refuses anything after the outer value, trailing newline included, so a body sent
    from a heredoc or a file, or pretty-printed, answered -32700 — every test.sh request among
    them. McpBase class>>parseBody: now strips trailing JSON whitespace (RFC 8259 §2 allows it)
    before the parse. Two unit tests that pinned a2-era kernel behaviour now accept a3's: the kernel
    writer's corrected surrogate pair, and parsed keys that are Unicode7 where
    #StringConfiguration is Unicode16.

  • eval_python runs as __main__ (#50).
    Its namespace was empty, so __name__ was a NameError and a class from type() or
    Enum('F', 'A B') had no __module__ (repr read <class 'T'>). The toolset is the launcher,
    as python -c or the REPL is in CPython, so a fresh namespace now holds __name__ = '__main__'.
    The __module__ half needs a Grail that reads __name__ from the caller's globals
    (GemTalk/Grail#1134).

  • find_python_senders places each reference in a method compiled direct to IR
    (#48). A references hit there read
    line ? [compiled to IR: no call-site positions], once per method. Each reference now gets its
    own hit with its Python line, from the same kernel send offsets that place calls, so
    (helper, helper) is two hits. A store to a module global (helper = 2, +=, for helper in,
    :=), which the literal frame had reported as a reference, is no longer one. A method where no
    send lands on the name (del helper, with … as helper) is still reported once, unplaced.

  • A deployment can replace the server's instructions. The stock initialize instructions
    name the transaction tools, so a router whose toolsetNames leave those tools out told the model
    about tools it did not have. They are now router config: McpRouter>>serverInstructions: (or
    MCP_INSTRUCTIONS_FILE in run-server.sh / run-auth-server.sh). The value replaces the class
    default, '' sends none, and anything over 32,768 characters is refused where it is configured.
    Breaking: the worker bootstrap selector is now
    prepareWorkerWithToolsets:options:serverName:title:version:instructions:frontEnd:cacheName:.

  • The stock instructions are shorter, and tell the model how to search a production database.
    The transaction text is tightened throughout. A new paragraph steers the model away from a loop
    over application objects, which can take hours, and toward the application's own collections, their
    indexes and a selection block.

  • find_python_senders places each call in a method compiled direct to IR. IR became Grail's
    default in GemTalk/Grail#1087, which turned most
    compiled hits into line ? [compiled to IR: no call-site positions], one per name a method
    called, so two calls on one line read as one. Each call now gets its own hit with its Python line
    and text, the same answer the text path gives. The line comes from the kernel's send offsets,
    rebased to a module line by BaseException pythonPositionsForMethod:, which also supplies its
    text; that rebase is what keeps it working since
    GemTalk/Grail#1164 stopped padding an IR method's
    source.

  • find_python_senders finds first-class references in methods compiled direct to IR. The
    references shape matched markers in generated Smalltalk, which an IR method does not have, so
    g = self.helper there answered nothing without saying it could not look. It now reads the
    method's literal frame, where each referenced name survives as a Symbol and attribute stores,
    keyword names and strings stay Strings. Each such method is reported once, as
    line ? [compiled to IR: no call-site positions].

  • A find_python_senders reference hit names the method it is in. Its (Class>>… env 1)
    suffix printed the generated-code marker the hit was found by (___pyAttrLoad___:), which names
    no method, so get_method_source could not open it. It now prints the containing method's
    selector, as a compiled hit always has.

  • find_python_senders says when a method was compiled direct to IR, and stops losing its
    recursive calls.
    Under GRAIL_IR_CODEGEN a method carries no position store, and the
    search now asks Grail which kind of method it is instead of reading the Python as generated
    Smalltalk; its calls are now placed (see above). A recursive call in one was previously dropped
    as arity glue. A bare line ? now means only that a text-compiled source could not be read or
    placed.

  • find_python_senders searches nested and function-local classes. Its compiled-shape scope
    now comes from importlib pythonClasses
    (GemTalk/Grail#885) instead of the
    GrailCanonicalClasses registry, which records module-scope class statements only. A call from
    Outer.Inner or from a class defined inside a function used to be answered no senders. It is
    now found and labelled as Python prints it (mod.Outer.Inner.probe). The Smalltalk-defined bases
    Grail answers with it are searched too, labelled builtins.int and so on. The class count in the
    trailer is now a total rather than a floor, and the trailer names the one new gap Grail reports:
    after a Grail install resets the registry, a committed module's classes are missing until it is
    imported again. No tool, option or schema changed.

  • The Python browsing and search tools decode selectors through Grail.
    McpGrailToolset>>pythonNameOfSelector: now delegates to importlib pythonNameOfSelector:
    (GemTalk/Grail#884) instead of reimplementing the
    mangling rule. Grail's decoder answers nil where nothing its mangling emits could have
    produced the selector, which the local one could not say, so two invented names are gone:
    find_python_senders for at no longer matches the plain Smalltalk selector at:put:, and a
    Grail-internal slot such as ___pyCallValue___:kw: no longer decodes to a two-underscore name
    that slipped past the internals screen and into a method listing. No tool, option or schema
    changed.

  • eval_python's stderr capture now reaches warm-bound modules. Nothing in this server
    changed: Grail moved sys's standard streams into session state
    (GemTalk/Grail#979), so a module warm-bound from a
    committed canonical instance — which hands out the committing session's sys, not this one's —
    now resolves stderr to the stream this tool installs. What such a module wrote used to be
    accepted, counted and dropped into a PyConsoleStream nobody reads; it now comes back in the
    [stderr] block. The README's and the method comment's account of that limitation is deleted,
    and a test holds the tool to the new behaviour.

  • Breaking: the kernel-class guard is gone. McpServer>>assertMutableClass: /
    assertRemovableDictionaryNamed: / isProtectedClass: / protectedDictionaryNames, the
    McpToolset forwarders of the first two, and McpFixtureServer's hardened override are all
    removed. The mutation tools no longer refuse a class for its home dictionary, and
    remove_dictionary no longer refuses Globals; a third-party toolset has no guard to call and
    no policy to override.

    It was the same mistake read-only mode was, one layer down: a refusal issued inside the process
    it claimed to bound. execute_code, a test body or a Python call reached past it in the next
    request, it had to be taught about symbol-list shadowing to stay correct in a Grail image, and its
    real cost was that it made a privileged session look restrained. GemStone has enforced this in the
    stone all along — kernel classes live in SystemObjectSecurityPolicy (#1: owner SystemUser write,
    world read), so a worker gem whose user lacks the ObjectSecurityPolicyProtection privilege is
    refused at the write with SecurityError 2257, whichever tool asked. Measured against
    setup-read-only-user.sh's user, which edits its own classes in UserGlobals in the same session.

    To migrate: if you were relying on the guard, give the worker gem a GemStone user that is
    actually restricted — MCP_WORKER_USER with ./setup-read-only-user.sh, or under McpAuthRouter
    a restricted UserProfile for the users in question. A subclass that overrode
    protectedDictionaryNames should drop the override; it is dead code now.
    docs/ReadOnly_User.md gained the measurements, and is the reference
    for what a user profile does and does not bound.

  • Breaking: read-only mode is gone, replaced by the worker gem's GemStone user. McpRouter's
    readOnly flag, MCP_READONLY, McpServer class>>sessionReadOnly: /
    coreReadOnlySafeToolNames, McpServer>>isReadOnly / isToolAllowed:, every toolset's
    readOnlySafeToolNames, the dispatcher's gating and its readOnly error kind, and
    McpAuthRouter's writeScope / MCP_WRITE_SCOPE are all removed. tools/list is now
    unfiltered and no tool is refused for being "unsafe".

    The gate could only ever be advisory: execute_code evaluates arbitrary Smalltalk, run_test_class
    runs arbitrary test bodies, and a tool that compiles can be followed by one that runs. The danger
    was that it looked like an access-control boundary in the one place that mattered — an
    administrator starting MCP_READONLY=1 ./run-server.sh on a privileged user.

    In its place, McpRouter>>workerUserId (MCP_WORKER_USER) names the GemStone user every worker
    gem logs in as; it defaults to the front end's own user, so an unconfigured router behaves as
    before. ./setup-read-only-user.sh provisions one that cannot commit
    (UserProfile>>disableCommits, which covers forked gems too) and cannot reach the host. No
    credential is configured — the front end mints a one-time password per session against a single
    committed grant — so the router carries only an identifier. McpAuthRouter refuses
    workerUserId:: there the bearer token names the user, so restricting someone means restricting
    that GemStone UserProfile.

    To migrate: replace MCP_READONLY=1 ./run-server.sh with ./setup-read-only-user.sh once,
    then MCP_WORKER_USER=McpReadOnly ./run-server.sh. Replace MCP_WRITE_SCOPE with a read-only
    UserProfile for the users who should not write. Narrowing toolsetNames still narrows what is
    offered, and is worth doing — it is just not a security control.
    docs/ReadOnly_User.md is the new reference: every privilege, the
    risk of granting it, the cost of withholding it, and what remains open afterwards (broad reads;
    a session can take a write lock that blocks other sessions' commits; resource use).

  • The [session] line no longer tells a commit-locked session to commit. Where
    System sessionCanCommit is false, pending work is reported as uncommittable and the line points
    at abort — previously it advised commit, which such a session can only ever fail.

  • The stone-version check no longer refuses every 4.0.0.Alpha1 stone. gs_env_require_stone
    compared gslist's version against one parsed out of $GEMSTONE/version.txt, and both halves were
    wrong for a version that is not purely numeric: the parse stopped at the first letter and answered
    4.0.0., while gslist reports 4.0.0.Alpha because the lock file it reads carries the version in
    a fixed twelve-byte field. So install.sh, run-server.sh and run-unit-tests.sh all refused a
    4.0.0.Alpha1 stone under its own product tree, with error: stone 'x' is version 4.0.0.Alpha, but GEMSTONE is 4.0.0., and no environment could satisfy them. The version is now read as line 2's
    whole first field, and a version at the eleven-character limit is compared as a prefix — so a
    genuine mismatch (3.7.5 against 4.0.0.Alpha1, either way round) is still refused exactly, and still
    names the stones that would work. With this, all 463 unit tests pass on 4.0.0.Alpha1.

  • GemStone 4.0 is now tested, and it is where the Grail toolset is tested. CI gains a 4.0
    leg, both plain and --grail, downloaded from dl.gemdb.com, where the pre-release is
    published. It runs 4.0.0.a5 (see the Rowan entry above). The pinned alpha can expire, since
    dl.gemdb.com has dropped old alphas before: the 4.0.0.Alpha1 directory answers 404. From
    4.0.0.a3 a stone no longer falls back to the bundled starter key, so a fresh 4.0 install must name
    it as KEYFILE in system.conf (see
    GemStone_Notes.md). No 3.7 leg carries
    Grail: Grail dropped 3.7.x on 2026-09-12 and its installer refuses the image, so the Grail toolset
    is covered on the one image that can load it.

Installing on GemStone 3.7.x without Rowan

Download mcp_server-0.9.0.gs below and this release's source code archive,
unpack the archive, and from its directory run:

./install.sh --from-gs /path/to/mcp_server-0.9.0.gs

The file is the Core and Auth components with their unit tests, tested on
3.7.5 and 3.7.6 before this release was published. On a Rowan image, install from the
source with ./install.sh instead. See the README's Install & run.