Skip to content

feat: support MCP Server resource (CREATE MCP SERVER) and MCP server grants#35

Open
GClunies wants to merge 2 commits into
datacoves:mainfrom
GClunies:snowcap-26
Open

feat: support MCP Server resource (CREATE MCP SERVER) and MCP server grants#35
GClunies wants to merge 2 commits into
datacoves:mainfrom
GClunies:snowcap-26

Conversation

@GClunies

@GClunies GClunies commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a schema-scoped mcp_server resource so Snowflake managed MCP servers can be managed declaratively instead of with hand-run SQL — where re-running CREATE OR REPLACE MCP SERVER silently drops every USAGE grant on the server. Snowflake has no ALTER MCP SERVER, so the resource diffs a canonicalized specification: an unchanged spec is a no-op plan, and only a real change emits a single CREATE OR REPLACE, with a plan-time warning that grants will be dropped (snowcap-managed grants come back on the next apply).

Closes #26

Changes

  • New MCPServer resource (mcp_servers: YAML key) with a specification property rendered as CREATE MCP SERVER <fqn> FROM SPECIFICATION $$...$$ (reuses StringProp, no new prop machinery)
  • Specification canonicalization: representation-independent across YAML config and DESC MCP SERVER's JSON server_spec (sorted keys, version defaulted to 1, YAML-only scalar types collapsed, tools sorted by name) — unit-tested against YAML 1.1 gotchas and all five tool types
  • update_mcp_server lifecycle: spec change → one CREATE OR REPLACE; rename → clear error (no ALTER exists); plan warning fires on both snowcap apply and the snowcap plan -o / apply --plan two-step workflow
  • Grants: GRANT { USAGE | MODIFY } ON MCP SERVER via on_mcp_server: or on: mcp server <fqn> (generic — zero grant.py changes); MCPServerPriv + CREATE MCP SERVER schema priv registered
  • Fetch/plan round-trip: fetch_mcp_server (SHOW + DESC, existence_only fast path) and list_mcp_servers
  • Docs: generated docs/resources/mcp_server.md, SUMMARY entry, MCP examples in grant.md
  • Tests: fixture pair (auto-swept by gitops/identity/from_sql suites), TestMCPServer, grant tests, lifecycle/warning tests — full unit suite green (1560)

Notes

  • Integration coverage (live SHOW/DESC round-trip) needs a Snowflake account with MCP servers; the normalizer's version-stamping assumption should be verified there before relying on no-op plans in production
  • Grant preservation: COPY GRANTS was tested against a live account and is not supported for CREATE OR REPLACE MCP SERVER (syntax error both after the name and after the specification). Instead, diff() injects re-creates for every snowcap-managed grant targeting a spec-changed server into the same plan, so managed grants are restored in the same apply (visible as explicit + grant entries in plan output); the warning now covers only externally-managed grants

GClunies added 2 commits July 16, 2026 23:13
Snowflake managed MCP servers (CREATE MCP SERVER ... FROM SPECIFICATION)
previously had to be managed with hand-run SQL, where re-running
CREATE OR REPLACE silently drops all USAGE grants on the server.

Add a schema-scoped mcp_server resource (mcp_servers: YAML key) with a
specification property. Because Snowflake has no ALTER MCP SERVER, the
specification is canonicalized (sorted keys, version defaulted, JSON/YAML
representation-independent, tools sorted by name) so an unchanged spec
produces a no-op plan; only a real change emits a single CREATE OR REPLACE,
and the plan warns that grants on the server will be dropped - on both the
one-step apply and the plan-file two-step workflow. Renames raise a clear
error instead of emitting invalid ALTER SQL.

GRANT USAGE ON MCP SERVER works via on_mcp_server: and the multi-word
on: string form with no grant.py changes; MCPServerPriv registers
MODIFY/USAGE. fetch_mcp_server reads SHOW + DESC (server_spec) with an
existence_only fast path; list_mcp_servers enables sync/export sweeps.

Closes datacoves#26
…c replace

CREATE OR REPLACE MCP SERVER drops all grants on the server, and COPY GRANTS
is not supported for this object type (verified against a live account:
syntax error in both grammatical positions). Previously snowcap-managed
grants only came back on the NEXT apply, leaving a window where MCP clients
lose access.

diff() now detects a specification-change UpdateResource on an MCP server
and appends CreateResource changes for every manifest object-grant targeting
that server (skipping grants that already have a change in the plan). Grants
execute after their target in the dependency order, so managed grants are
restored in the same apply. The plan output shows the re-grants explicitly,
and the grant-drop warning now only concerns externally-managed grants.
@noel

noel commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Thanks @GClunies — this is a well-engineered solution to a genuinely awkward constraint (CREATE OR REPLACE silently dropping grants). I checked out the branch and verified the key mechanics:

  • Symmetric normalization confirmed. Remote state is built via resource_cls.spec(**data) (blueprint.py:1274), so the fetched server_spec runs through the same normalize_mcp_specification as the manifest — which is what makes "unchanged spec = no-op plan" actually hold. test_unchanged_spec_recreates_nothing proves it.
  • Ordering is safe. The re-created grant carries a dependency ref on the server, so compute_levels orders it after the CREATE OR REPLACE — not reliant on the append order in diff().
  • Ran test_blueprint / test_grant / test_lifecycle / test_resource_types: 346 passed.

The TestMCPServerSpecChangeRegrantsManagedGrants coverage is excellent — spec-change re-grants, unchanged = empty plan, and the no-duplication guard are all exercised.

Requested before merge

  1. One live SHOW/DESC round-trip. The no-op-plan guarantee rests on DESC MCP SERVER's server_spec normalizing byte-identically to config. If a real account returns extra keys, a different version, or ordering the normalizer doesn't collapse, every plan will show drift. The unit tests mock this — please confirm against a live account with an MCP server before we rely on it. (You already called this out in the PR notes; just want it closed before merge.)

Requested changes (minor)

  1. Guard malformed tools. sorted(data["tools"], key=lambda t: t["name"]) raises a raw KeyError/TypeError if a tool is missing name or isn't a dict. A clear "each tool requires a name" error would be friendlier.
  2. Confirm the existence_only path. fetch_mcp_server(existence_only=True) returns no specification; please confirm that dict never reaches resource_cls.spec(**data) (it would TypeError on the missing required arg).

Nit

  • apply() now calls fetch_session even when plan is None, after which plan() fetches it again — harmless double fetch.

Looks good

  • Rename explicitly blocked with a clear error rather than silently doing the wrong thing.
  • Grants reuse the generic machinery (zero grant.py changes).
  • Honest docstring — documenting the YAML 1.1 yes/no/on/off boolean gotcha and comment-stripping is exactly right.

Nice work — this is close.

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.

feat: support MCP Server resource (CREATE MCP SERVER) and MCP server grants

2 participants