Skip to content

Add gam mcp: an MCP server for GAM (#1855) - #1978

Open
gedigi wants to merge 2 commits into
GAM-team:mainfrom
gedigi:mcp-server
Open

gedigi wants to merge 2 commits into
GAM-team:mainfrom
gedigi:mcp-server

Conversation

@gedigi

@gedigi gedigi commented Sep 3, 2026

Copy link
Copy Markdown

Closes #1855.

What this adds

gam mcp [allowwrites] [maxrows <Number>] [timeout <Number>] [nowiki] serves the Model Context Protocol over stdin/stdout, so an AI assistant (Claude Desktop, Claude Code, Gemini CLI, ...) that spawns gam mcp gets three tools and a set of resources:

  • gam_syntax - keyword search over GamCommands.txt; returns the verbatim syntax block, its section, and the definitions of the non-terminals it references. This is the direct answer to mcp server? #1855.
  • gam_docs - read a wiki page or a section of it, or search page titles and headings.
  • gam_run - run one GAM command in-process and return rc, stdout, stderr and, for print commands, the CSV rows as a list of objects. Read-only by default.
  • Resources gam://syntax, gam://syntax/<Section>, gam://wiki/<Page>.

Zero new dependencies: the transport and the JSON-RPC methods are standard library; fetches go through getHttpObj(). The module is loaded on demand with LazyLoader, like yubikey, and listed in hiddenimports.

Why this shape

A fixed set of tools cannot cover GAM. dbarks/mcp-gam wraps a dozen user, group and OU commands and will always cover a sliver of the roughly one thousand command shapes behind GAM's parser. A syntax lookup plus a guarded runner covers all of it, and it stays current because it reads GamCommands.txt and the wiki rather than a hand-written copy of them.

Safety model

  • Read-only by default. The command line is classified by walking the same dispatch tables as ProcessGAMCommand (MAIN_COMMANDS, MAIN_COMMANDS_WITH_OBJECTS, the calendar, course and resource subcommand tables, USER_COMMANDS*, CROS_COMMANDS*), stopping before any function is called and before any API is touched. The Act code from the table is the verdict: INFO, LIST, PRINT, SHOW, REPORT, CHECK, EXISTS, LOOKUP, COMMENT, GET_COMMAND_RESULT, plus version and help, are read-only; everything else requires allowwrites; DOWNLOAD is refused even then. Unresolvable command lines are refused, never guessed.
  • Always refused, wherever the word appears: batch, tbatch, csv, csvtest, loop, redirect, config, multiprocessexit, select ... save, oauth, audit, sendemail, sendreply, every file based entity selector (file, csvfile, datafile, csvkmd, csvsubkey, csvdata, crosfile*, croscsv*), and non read-only commands on project, svcacct and sakey objects. todrive is refused on a read-only server.
  • Execution uses the ProcessGAMCommandMulti capture pattern: StringIO buffers in GM.Globals[GM.STDOUT]/[GM.STDERR] and a collector in GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE], then CallGAMCommand. Rows come straight from the queue payload, no CSV text is parsed. One command at a time, with a timeout.
  • Nothing but JSON-RPC reaches stdout: the server keeps a private duplicate of fd 1 and points fd 1 at the null device for the life of the process, so GAM output, debug_level output and subprocesses cannot corrupt the channel. Access and refresh tokens are redacted from captured output.
  • The server's instructions tell the assistant that it acts as one administrator, that a single command can touch the whole domain, to confirm targets before writes, that truncated rows are not totals, and that names, descriptions and file names are user-set text to be treated as data.

Docs sourcing

GamCommands.txt is read from the GAM folder (it ships with every release) or from src/ in a checkout; a pip install gam7 falls back to fetching it from GitHub once a day into <cache_dir>/mcp/. Wiki pages come from wiki/ in a checkout, else from raw.githubusercontent.com/wiki/GAM-team/GAM/ with a 24 hour cache; nowiki disables fetches for air-gapped installs. Nothing from the wiki is bundled.

Protocol

Both eras of the specification are served: the per-request _meta model with server/discover (2026-07-28) and the initialize handshake (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05). Result shapes and error codes follow whichever the client uses.

How to try it

claude mcp add gam -- /path/to/gam mcp

or, for Claude Desktop and Gemini CLI, {"mcpServers": {"gam": {"command": "/path/to/gam", "args": ["mcp"]}}}. Full configuration and examples are in the new wiki page MCP-Server. Without an assistant:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"gam_syntax","arguments":{"query":"print filelist","limit":1}}}' \
  | gam mcp

Files changed

  • src/gam/gamlib/glmcp.py - new; transport, docs index and cache, classifier, runner, protocol dispatch, in separate classes.
  • src/gam/__init__.py - LazyLoader line, doMCPServer next to doUsage, 'mcp' in MAIN_COMMANDS.
  • src/gam/gamlib/glmsgs.py - new MCP_* messages.
  • src/gam.spec - hidden import.
  • src/GamCommands.txt - # MCP Server section after # Version and Help.
  • wiki/MCP-Server.md, wiki/_Sidebar.md - new page, linked under Command Processing.
  • src/GamUpdate.txt - entry under 7.49.00; renumber as you see fit, __version__ is untouched.
  • .github/workflows/build.yml - a stdio smoke test after the version check on every job: initialize, tools/list, a gam_syntax call and a gam_run of version simple, asserting the three tool names, the syntax block, the version string, and that stdout holds nothing but JSON-RPC. No secrets, no network.

Open for feedback

  • Argument names: allowwrites, maxrows, timeout, nowiki.
  • Whether to bundle the wiki (4.9 MB) instead of fetching and caching it.
  • The read-only Act set, in particular COMMENT and GET_COMMAND_RESULT.
  • Defaults: maxrows 500, timeout 300.
  • Whether todrive should ever be allowed, and whether sendreply, enable apis and the project/svcacct/sakey writes should stay refused with allowwrites.
  • File selector words are refused wherever they appear, which also refuses getcommand|issuecommand ... csv; a position-aware rule would be possible if that matters.
  • gam_run is not a filesystem sandbox: options such as localfile or targetfolder are not blocked; the wiki page says so. Happy to enumerate more if you prefer.
  • Whether to register mcp as a [project.scripts] alias.
  • On a brand new installation SetGlobalVariables() prints "Config File ... Initialized" to stdout before any command runs, so the first ever gam mcp breaks the transport; the wiki says to run gam version once. Routing that message to stderr would remove the caveat but touches SetGlobalVariables(), so I left it alone.

Serve the Model Context Protocol over stdin/stdout with three tools:
gam_syntax searches GamCommands.txt, gam_docs reads and searches the
wiki, gam_run executes a command in-process, read-only unless the
server is started with allowwrites. Standard library only; the module
is loaded on demand like the YubiKey support.
@jay0lee

jay0lee commented Sep 3, 2026

Copy link
Copy Markdown
Member

My standard policy on AI pull requests:

I need a human being to respond with:

  1. what they're trying to do. What real-world problem were you trying to solve? How does this benefit other GAM users?
  2. how exactly the PR was generated (model, prompts, etc).

@gedigi

gedigi commented Sep 3, 2026

Copy link
Copy Markdown
Author

Sorry for not saying this in the description: the PR was generated with Claude Code (model Claude Fable 5.1), from a brief I wrote and with me reviewing at checkpoints.

1. What I'm trying to do. I admin a Workspace domain and already use an assistant to work out GAM command lines from the wiki. The assistant gets syntax wrong from memory, which is exactly the risk Zircoz raised on
#1855. This makes it answer from GamCommands.txt and the wiki instead, and lets it run commands only through a gate built on GAM's own dispatch tables, read-only unless started with allowwrites. Other users get that with any MCP client and nothing to install beyond GAM.

2. How it was made. I wrote a brief fixing the constraints: one PR, no new dependencies, GAM's conventions (LazyLoader and hiddenimports like YubiKey, strings in glmsgs), docs read from your files rather than bundled, and in-process execution via CallGAMCommand with the ProcessGAMCommandMulti capture pattern. The model had to verify every claim against the tree at 9716968 and read the MCP spec pages rather than guess. I stopped it at four checkpoints and decided the open questions (dual-era protocol, the read-only Act set, sidebar placement, changelog version). Tests: the CI smoke step from source and against a local PyInstaller build, protocol error cases, the classifier over all 1,050 syntax lines, a Python 3.10 compile check. No test domain, so API calls were only checked to fail cleanly. Happy to post the brief as a gist.

I did not write the code by hand. If a PR this size from an AI is not something you want, the gam_syntax lookup alone is separable and much smaller, and I can cut it down to that.

The server could exit before a deferred gam_run reply was written when
the client closed stdin right after the request. Count accepted commands
and wait until each has replied. Redact quoted token values without
dropping their quotes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp server?

2 participants