Repository navigation
mcp_server 0.9.0
-
The server is now a Rowan project,
Mcp, in Tonel. Breaking: installing needs a stone on a
Rowan 3 extent (extent0.rowan3.dbf);install.shrefuses 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 theCore,Authand optionalGrailcomponents underrowan/, and the
.gsgroups and their loaders are gone. Changes are made in the image and written out with the new
./write-project.sh.install.shmigrates an image installed from.gs.--grailnow loads the
Grailcomponent, 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.shwritesCore+
Authas onemcp_server.gswith GemStone's own Rowan exporter (no stone needed), and
./install.sh --from-gs <file>files it into a stockextent0.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 itsextent0.rowan3.dbf, with and without
Grail. Pushing av<version>tag onmainnow 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.topaziniin the
checkout madeinstall.shreport 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 tolocalhostbut took the stone's from the machine's host name
whenever the launching login named none — asrun-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 suiteMcpWorkerLoginTest. -
find_python_senderstakes 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_methodends 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.shwrote 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 onlyError,
and not everything the kernel parser can signal is one. It now catches the kernel'sException,
looked up throughGlobalsbecause a Grail image binds#Exceptionto Python's class first. -
A request body ending in whitespace is no longer a parse error on GemStone 4.0.0.a3. a3's
JsonParserrefuses anything after the outer value, trailing newline included, so a body sent
from a heredoc or a file, or pretty-printed, answered-32700— everytest.shrequest 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 areUnicode7where
#StringConfigurationisUnicode16. -
eval_pythonruns as__main__(#50).
Its namespace was empty, so__name__was aNameErrorand a class fromtype()or
Enum('F', 'A B')had no__module__(reprread<class 'T'>). The toolset is the launcher,
aspython -cor 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_sendersplaces each reference in a method compiled direct to IR
(#48). Areferenceshit 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 stockinitializeinstructions
name the transaction tools, so a router whosetoolsetNamesleave those tools out told the model
about tools it did not have. They are now router config:McpRouter>>serverInstructions:(or
MCP_INSTRUCTIONS_FILEinrun-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
instructionsare 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_sendersplaces each call in a method compiled direct to IR. IR became Grail's
default in GemTalk/Grail#1087, which turned most
compiled hits intoline ? [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 byBaseException 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_sendersfinds first-class references in methods compiled direct to IR. The
referencesshape matched markers in generated Smalltalk, which an IR method does not have, so
g = self.helperthere 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_sendersreference 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, soget_method_sourcecould not open it. It now prints the containing method's
selector, as a compiled hit always has. -
find_python_senderssays when a method was compiled direct to IR, and stops losing its
recursive calls. UnderGRAIL_IR_CODEGENa 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 bareline ?now means only that a text-compiled source could not be read or
placed. -
find_python_senderssearches nested and function-local classes. Its compiled-shape scope
now comes fromimportlib pythonClasses
(GemTalk/Grail#885) instead of the
GrailCanonicalClassesregistry, which records module-scope class statements only. A call from
Outer.Inneror from a class defined inside a function used to be answeredno 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, labelledbuiltins.intand 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 toimportlib 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_sendersforatno longer matches the plain Smalltalk selectorat: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 movedsys'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'ssys, not this one's —
now resolvesstderrto the stream this tool installs. What such a module wrote used to be
accepted, counted and dropped into aPyConsoleStreamnobody 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
McpToolsetforwarders of the first two, andMcpFixtureServer's hardened override are all
removed. The mutation tools no longer refuse a class for its home dictionary, and
remove_dictionaryno longer refusesGlobals; 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 inSystemObjectSecurityPolicy(#1: owner SystemUser write,
world read), so a worker gem whose user lacks theObjectSecurityPolicyProtectionprivilege is
refused at the write withSecurityError2257, whichever tool asked. Measured against
setup-read-only-user.sh's user, which edits its own classes inUserGlobalsin 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_USERwith./setup-read-only-user.sh, or underMcpAuthRouter
a restrictedUserProfilefor the users in question. A subclass that overrode
protectedDictionaryNamesshould 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
readOnlyflag,MCP_READONLY,McpServer class>>sessionReadOnly:/
coreReadOnlySafeToolNames,McpServer>>isReadOnly/isToolAllowed:, every toolset's
readOnlySafeToolNames, the dispatcher's gating and itsreadOnlyerror kind, and
McpAuthRouter'swriteScope/MCP_WRITE_SCOPEare all removed.tools/listis now
unfiltered and no tool is refused for being "unsafe".The gate could only ever be advisory:
execute_codeevaluates 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 startingMCP_READONLY=1 ./run-server.shon 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.shprovisions 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.McpAuthRouterrefuses
workerUserId:: there the bearer token names the user, so restricting someone means restricting
that GemStone UserProfile.To migrate: replace
MCP_READONLY=1 ./run-server.shwith./setup-read-only-user.shonce,
thenMCP_WORKER_USER=McpReadOnly ./run-server.sh. ReplaceMCP_WRITE_SCOPEwith a read-only
UserProfile for the users who should not write. NarrowingtoolsetNamesstill 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 sessionCanCommitis false, pending work is reported as uncommittable and the line points
atabort— previously it advisedcommit, 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
comparedgslist'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., whilegslistreports4.0.0.Alphabecause the lock file it reads carries the version in
a fixed twelve-byte field. Soinstall.sh,run-server.shandrun-unit-tests.shall refused a
4.0.0.Alpha1 stone under its own product tree, witherror: 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 fromdl.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.comhas 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 asKEYFILEinsystem.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.gsThe 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.