mcp_server

Description

MCP Server for GemStone

Details

Source
GitHub
Dialect
gemstone (25% confidence)
Created
Aug. 19, 2026
Updated
Sept. 25, 2026

Categories

Serialization

README excerpt

# Native GemStone MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io) server written natively in
GemStone Smalltalk. It runs **inside** the image and executes tool calls directly — no
Node.js process, no GCI/FFI bridge. The goal is to replace the GCI-based Jasper MCP
server with one that any MCP client can reach over plain HTTP.

**Status:** the Streamable HTTP transport, per-client worker gems, 31 base tools (+9 optional
Python), OAuth 2.1 / JWT + TLS, per-router worker identity and server-initiated messages are built
and verified end-to-end — by curl, by a TLS run, and by the in-image suites. What is *not* built is
listed under [Future work](#future-work).

**A note about versions.** The supported images are **3.7.5 and 3.7.6+**, whose implementation of
`System continueTransaction` the server's view handling relies on — see
[docs/GemStone_Notes.md](docs/GemStone_Notes.md#version-to-version-differences) for the
measurements. Within that range what you get depends on the image, and `install.sh` works it out
for you:

| image | base server (`run-server.sh`) | OAuth/OIDC front end (`run-auth-server.sh`) |
|---|---|---|
| **3.7.5** | yes | yes, against a local IdP |
| **3.7.6+** | yes | yes, including an external OIDC IdP |

`src/auth` needs `JsonWebToken` and `JwtSecurityData` (and, to log a worker in,
`GsTsExternalSession>>jwtPassword:`), none of which exist before 3.7.5, so `install.sh` detects what
the image has rather than asking. The 3.7.6 line is a separate matter: earlier releases have a bug
connecting to an *external* OIDC IdP.

## Other documentation

This README is the reference for what the server is and how to run it. Alongside it:

* [CHANGELOG.md](CHANGELOG.md) — what changed per release.
* [docs/Development.md](docs/Development.md) — **start here to contribute**: environment, the
  per-change loop, canonical file-outs, testing, branches and releases.
* [docs/GemStone_Notes.md](docs/GemStone_Notes.md) — image and kernel behaviour that has cost this
  project time, most of it invisible in the source. Its first section is the silent failures.
* [docs/MCP_Client_Notes.md](docs/MCP_Client_Notes.md) — measured client behaviour, and where the
  protocol revisions contradict each other.
* [docs/blind-write-guardrail.md](docs/blind-write-guardrail.md),
  [docs/session-lifetime.md](docs/session-lifetime.md), [docs/utf8-wire.md](docs/utf8-wire.md) —
  deep dives on three subsystems.
* [.claude/CLAUDE.md](.claude/CLAUDE.md) — the same process as `docs/Development.md`, condensed for
  a coding agent with the traps attached. Machine-specific facts go in a gitignored
  `.claude/CLAUDE.local.md` (template: `.claude/CLAUDE.local.md.example`).

## Install & run

```bash
export GEMSTONE=/path/to/GemStone64Bit3.7.x   # product dir

./install.sh --check                # verify the environment and report what would be installed
./install.sh                        # file in the classes and commit; auth included if the image can
./install.sh --auth                 # ...and fail loudly if it cannot, instead of quietly skipping
./install.sh --no-auth              # ...or leave the auth group out of an image that could take it
./install.sh --grail                # ...plus the optional Grail/Python toolset (Grail image only)
MCP_PORT=8000 ./run-server.sh       # fork a detached, independent localhost server gem and return
MCP_WORKER_USER=McpReadOnly ./run-server.sh   # ...every worker gem as a restricted GemStone user
MCP_TOOLSETS="McpBrowsingToolset McpSearchToolset" ./run-server.sh   # ...only these tools
MCP_WORKER_CLASS=MyMcpServer ./run-server.sh                         # ...a subclass as the worker

# ...the core tools PLUS the optional Python toolset. Naming it is what turns it on: installing
# src/grail puts McpGrailToolset in the image, and nothing more. Same shape for your own toolset.
MCP_TOOLSETS="McpBrowsingToolset McpExecutionToolset McpListingToolset McpMutationToolset \
  McpSearchToolset McpSessionToolset McpTestingToolset McpGrailToolset" \
  MCP_GRAIL_DIR=/path/to/Grail ./run-server.sh
MCP_TRACE=1 ./run-server.sh         # ...logging every message a client sends (see Message trace)
./run-auth-server.sh                # ...the OAuth/OIDC network-facing server (McpAuthRouter)
```

`install.sh` and the `run-*.sh` scripts use topaz; set `GEMSTONE`, `GS_STONE`, `GS_USER`,
`GS_PASS` to match your environment — and read **Environment** below before assuming those four are
enough, because on many machines they are not. `install.sh` files the code in with topaz, one
group at a time: `src/core` and `src/tests` always, `src/auth` when the image can compile it, and
`src/grail` on `--grail` (or `MCP_WITH_GRAIL=1`).

The two optional groups are selected differently on purpose. Loading `McpAuthRouter` is inert —
nothing instantiates it until you fork one with `run-auth-server.sh` — so it can be detected rather
than asked about, and `install.sh` probes the image for `JsonWebToken` to decide. Loading
`McpGrailToolset` is inert too, deliberately: it is **not** in the default tool surface
(`McpServer class>>defaultToolsetNames`), so filing it in adds a class to the image and changes no
server. It nonetheless stays **opt-in at install**, because the probe that works for auth does not
work here — `ModuleAst` being present does not mean the Grail these tools were written against, and
a group that cannot compile takes the file-in down with it. Use `--auth` to turn a skip into an
error, `--no-auth` to force one; `--check` reports the decision without installing anything.

`run-server.sh` builds a base `McpRouter` instance and calls
its `forkOnPort:` (`run-auth-server.sh` builds an OIDC-configured `McpAuthRouter` — resource-server
config as code, no commit), which launches a detached, independent front-end gem and returns; stop it
with `./stop-server.sh` (by port), or the `System stopSession: <id>` / `kill <pid>` line it prints.
The front end resolves the tool surface per session: `MCP_TOOLSETS` if it is set, otherwise the core
seven and nothing else. **No toolset joins that surface by being loaded** — an optional one, this
project's `McpGrailToolset` included, is served only by a router that names it. That keeps having a
toolset in the image and running it as two decisions, which matters when the toolset carries a
dependency of its own: Grail's tools read the checkout on disk that `MCP_GRAIL_DIR` names.

### Environment

Every script here sources `gs-env.sh`, which resolves the environment and refuses to continue on a
misconfigured one. `--check` runs that resolution and reports without doing anything else; it is the
first thing to run on a machine you have not installed on before.

```
$ ./install.sh --check
product      /opt/gemstone/GemStone64Bit3.7.5-x86_64.Linux
global dir   /opt/gemstone

servers visible to this client:
  Status     Version   Owner        Pid  Port  Started      Type    Name
  OK         3.7.5     gsadmin    96453 65166 Aug 23 12:31  Netldi  gs64ldi
  OK         3.7.5     gsadmin    60042 56820 Aug 20 08:53  Stone   gs64stone

OK: environment looks usable for gs64stone.
```

**`GEMSTONE_GLOBAL_DIR` is the variable that decides whether anything works**, and it is the one the
old four-variable advice left out. Get it wrong and every script fails at `login` with:

```
could not find server 'gs64stone' on host 'somehost' because service not found,
getaddrinfo failed, EAI error 8   ... Number: 4065
```

That message names `getaddrinfo`, so it reads like a DNS or `/etc/services` problem. It is not.
With no `/etc/services` entries a stone and a netldi each bind an **ephemeral** port and record it
in `$GEMSTONE_GLOBAL_DIR/locks/<name>..LCK`; clients read those lock files. A client pointed at a
different `GEMSTONE_GLOBAL_DIR` than the stone was *started* with finds no lock file and falls back
to a hostname/service lookup, which fails. The product's built-in default is `/opt/gemstone` (then
`/usr/gemstone`), so any installation keeping its locks elsewhere must tell its clients where.

`.setenv.example` is a starting point: copy it to `.setenv` (git-ignored) and edit it for your
machine. Most of it is optional — `gs-env.sh` discovers it rather than making you guess: it asks `gslist` under each candidate and
uses the one where the running servers actually are, saying so when it has to correct or supply a
value. `gslist` is the authority here — it reads the same lock files the GCI client does.

Do **not** reach for `/etc/services`. Registering a stone or netldi there is unnecessary once
`GEMSTONE_GLOBAL_DIR` is right, and it is a trap: netldi binds the port named in `/etc/services`
only if it is **restarted** after the entry exists, so an entry added to a running system is stale
by construction and points at a port nothing is listening on.

**Which scripts need a netldi.** `install.sh` talks only to the stone, so it runs fine on a host
with no netldi at all. The `run-*.sh` scripts need one — not because of how they log in, but
because `McpRouter>>forkOnPort:` and every per-client worker create a `GsTsExternalSession`, and
netldi is what forks those gems. `run-unit-tests.sh` needs one too: four of
its suites spawn a real worker gem (`McpExternalSessionTest`, `McpTransactionTest` and
`McpWorkerDeadlineTest` always, `McpAuthTest` where the auth group is installed). It asks the image which are present rather than
insisting unconditionally. Each script checks for what it actually needs, and says which is missing.

**Linked vs RPC.** These scripts run `topaz -l` (linked). That is deliberate, and it is not the
cause of the error above: a linked login resolves the stone through the same lock files, so it
needs `GEMSTONE_GLOBAL_DIR` and nothing else — no netldi, no NRS, no service entries. Dropping `-l`
routes the login through netldi instead, which works equally well once `GEMSTONE_GLOBAL_DIR` is
right, but it would make `install.sh` depend on a netldi it otherwise has no use for.


## Transport

A single e
← Back to results