Skip to main content

static-hosting-mcp

CI PyPI License: MIT

A Python stdio MCP server that publishes a one-off artifact — a research report, a plan, some rendered HTML — to a Google Cloud Storage bucket and hands back a permanent, access-controlled URL, plus tools to grant and revoke read access by Google-account email. Bucket and service-account credentials are loaded from the environment and are never exposed to the LLM.

The returned URL is a permanent authenticated URL of the form https://storage.cloud.google.com/<bucket>/<object_key>. It opens for any Google account you grant read access to (plus anyone with inherited bucket/project read, including the server's own service account) and is denied to everyone else. It does not expire.

Access model

  • Authenticated URLs + per-object ACL grants. Each artifact gets a permanent authenticated URL; access is granted per object by adding the recipient's email as an ACL reader. This is the one combination that is permanent and email-restricted — unlike public URLs (no access control), signed URLs (expire, 7-day cap), or bucket-wide IAM (can't scope a grant to one file).
  • Fine-grained access (UBLA off). Per-object ACLs require the bucket's Uniform Bucket-Level Access to be disabled. The server never changes this setting. On startup it validates only that the credentials load and the bucket is reachable; it does not read the UBLA setting then. If UBLA is on, the first per-object grant (on publish or grant_access) fails and the server returns an actionable error naming the bucket and the command to disable it.
  • Single role. The service account needs exactly one role on the bucket: roles/storage.objectAdmin (object read/write/delete + per-object ACL management). No bucket-metadata (storage.buckets.get) permission is required.

Operator setup

The server reads its configuration from the environment. The correct place to put it is a .env file at the project root (already gitignored), with the key file under secrets/ (also gitignored). .env.example documents the contract.

Environment contract

Var Required Purpose
GCS_BUCKET yes Bucket name only — no gs:// prefix (e.g. my-artifacts-bucket). Not a secret; it appears as the bucket segment of returned URLs.
GOOGLE_APPLICATION_CREDENTIALS yes Absolute path to the service-account JSON key (recommended: secrets/gcs-sa-key.json).
GCS_PROJECT_ID optional Overrides the project id; if unset it is derived from the key/ADC.
ARTIFACT_SOURCE_ROOT optional Absolute directory that publish_artifact's source_path reads are confined to. Unset ⇒ source_path is disabled (publish inline content only). Files that resolve outside it — and credential/secret shapes (the SA key, ~/.ssh, ~/.config/gcloud, *.pem/*.key, anything under a secrets/ directory) — are refused with no upload.
ARTIFACT_MAX_BYTES optional Maximum uploaded artifact size in bytes, inline or source_path (default 104857600 = 100 MiB). An oversized file is refused from a stat, before it is read into memory.

Why source_path is locked down. A published artifact can be shared with external Google accounts, so an unconfined local-file read would be a read-any-file → publish → share-to-attacker channel whose highest-value target is the service-account key this server otherwise keeps hidden. source_path is therefore default-denied: opt in per deployment with ARTIFACT_SOURCE_ROOT, and keep the key file (and any other secret) outside that directory. Inline content is unaffected.

1. Provision the bucket, service account, and key

Run once; substitute YOUR_PROJECT_ID / YOUR_BUCKET_NAME:

# 1. Create a bucket with fine-grained access (UBLA OFF) and public access prevented.
#    --no-uniform-bucket-level-access and --public-access-prevention are boolean flags
#    (no value). Per-recipient ACL grants are unaffected by public-access-prevention;
#    it only blocks public (allUsers / allAuthenticatedUsers) grants.
gcloud storage buckets create gs://YOUR_BUCKET_NAME \
  --project=YOUR_PROJECT_ID \
  --location=US \
  --no-uniform-bucket-level-access \
  --public-access-prevention

# If the bucket already exists with UBLA on, disable it instead:
# gcloud storage buckets update gs://YOUR_BUCKET_NAME --no-uniform-bucket-level-access

# 2. Create the server's service account.
gcloud iam service-accounts create static-hosting-mcp \
  --project=YOUR_PROJECT_ID \
  --display-name="static-hosting-mcp server"

# 3. Grant the single required role ON THE BUCKET:
#    objectAdmin = object read/write/delete + per-object ACL management.
SA="static-hosting-mcp@YOUR_PROJECT_ID.iam.gserviceaccount.com"
gcloud storage buckets add-iam-policy-binding gs://YOUR_BUCKET_NAME \
  --member="serviceAccount:${SA}" --role="roles/storage.objectAdmin"

# 4. Create and download a JSON key into secrets/ (gitignored).
gcloud iam service-accounts keys create secrets/gcs-sa-key.json --iam-account="${SA}"

roles/storage.objectAdmin on the bucket is the only role required. If a permission ever proves insufficient against the live API, roles/storage.admin on the bucket is the broad fallback.

2. Key-creation org-policy caveat & ADC fallback

If the project is under a Cloud organization that enforces constraints/iam.disableServiceAccountKeyCreation (the default for orgs created on/after 2024-05-03), step 4 will fail. For a consumer/Gmail account with no organization it works as written. Otherwise, grant a policy exception or switch to keyless Application Default Credentials (ADC):

# Keyless ADC — leave GOOGLE_APPLICATION_CREDENTIALS unset and authenticate locally:
gcloud auth application-default login
# (or use service-account impersonation)

With ADC the server builds its storage client from the ambient credentials instead of a key file; set GCS_PROJECT_ID if the project can't be inferred.

3. Configure the environment

cp .env.example .env
# then edit .env and fill in GCS_BUCKET and GOOGLE_APPLICATION_CREDENTIALS

Running locally

This is a local uv project. From the project root:

uv sync                      # resolve and install dependencies
uv run static-hosting-mcp    # start the stdio MCP server (reads .env)

On startup the server validates that the credentials load and the bucket is reachable, and aborts with an actionable message if either fails. If the required environment variables are missing it prints Missing required environment variables: ... to stderr and exits non-zero.

Claude Desktop / Claude Code config

Add the server to your MCP client config (Claude Desktop: claude_desktop_config.json; Claude Code: .mcp.json or ~/.claude.json). Use an absolute path to this checkout and to the key file:

{
  "mcpServers": {
    "static-hosting": {
      "command": "uv",
      "args": [
        "run",
        "--directory",
        "/absolute/path/to/static-hosting-mcp",
        "static-hosting-mcp"
      ],
      "env": {
        "GCS_BUCKET": "your-bucket-name",
        "GOOGLE_APPLICATION_CREDENTIALS": "/absolute/path/to/static-hosting-mcp/secrets/gcs-sa-key.json"
      }
    }
  }
}

The env block here is equivalent to the .env file; rely on either. Add GCS_PROJECT_ID only if it can't be derived from the key.

Tools

Six workflow-oriented tools. Each returns a curated dict; failures come back as a structured error dict ({"isError": true, "error": ...}) with a next step — tools never crash the session.

Publish & share (writes)

  • publish_artifact — Publish one self-contained artifact (inline content or a local source_path — exactly one) under a title, and optionally share it in the same call via grant_emails. Returns the permanent URL, object key, content-type, size, and per-email grant results. source_path requires ARTIFACT_SOURCE_ROOT to be set and the file to resolve inside it (else it is disabled); if the upload succeeds but a grant fails, the result still carries the recoverable key/url plus a warning (retry with grant_access, don't re-publish).
  • grant_access — Share an artifact that already exists: add per-object read access for one or more emails (by object key or full URL). Idempotent; returns per-email results; the URL is unchanged.
  • revoke_access — Remove read access for one or more emails from an existing artifact. Idempotent and destructive; returns per-email results.

Inspect & manage

  • list_artifacts — List published artifacts as curated summaries (key, URL, created date, size, grantee count), optionally filtered by a date_prefix (YYYY, YYYY/MM, or YYYY/MM/DD); default limit 50, with a truncation signal and a narrow-the-prefix / raise-the-limit hint. Read-only. Use this to find many.
  • get_artifact — Fetch one artifact's metadata (URL, content-type, size, created date, current grantee emails) by object key or full URL. Read-only.
  • delete_artifact — Delete one artifact (by object key or full URL); afterward its URL stops resolving. Destructive. v1 has no overwrite-in-place: to revise an artifact, delete and re-publish (this yields a new URL and requires re-granting).

Object reference

grant_access, revoke_access, get_artifact, and delete_artifact all accept the same object reference — either the bare object key (2026/06/24/q2-tariff-deep-research-7f3a9c.html) or the full returned URL.

Manual acceptance check

Restricted read is asserted automatically at the ACL layer and by confirming an unauthenticated fetch is denied (see the live test suite). The final human-in-the-loop check is manual, because the authenticated URL requires an interactive Google sign-in:

A granted recipient opens the https://storage.cloud.google.com/<bucket>/<key> URL while signed in to a granted Google account and sees the artifact; a person without a grant (or signed into a non-granted account) is denied.

Per-object ACLs cap at ~100 entries, so an artifact can be shared with up to ~100 accounts — ample for one-off sharing.

Development

uv run pytest                # fast unit tier (credential-free, faked GCS client)
uv run pytest -m live        # live integration against the configured bucket (skips if .env is incomplete)
uv run ruff check            # lint
uv run mypy src              # type-check

Metadata

Release files for static-hosting-mcp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for static-hosting-mcp 0.1.0
File Size Uploaded Provenance Actions
static_hosting_mcp-0.1.0.tar.gz 61.6 kB Without provenance Details for static_hosting_mcp-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for static-hosting-mcp 0.1.0
File Interpreter ABI Platform Actions
static_hosting_mcp-0.1.0-py3-none-any.whl Python 3 none any Details for static_hosting_mcp-0.1.0-py3-none-any.whl

Total release size: 95.1 kB

Release files / static_hosting_mcp-0.1.0.tar.gz

Download URL static_hosting_mcp-0.1.0.tar.gz
Size 61.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d386d9a662b54306697ea925b9db676de2b1ea109db01a577332651e70616bd0
BLAKE2b-256 checksum
How to use checksums
7ffac1e9bbbe2d869fa358814c11a7818449a5e78a808103e3f0768693a65654
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / static_hosting_mcp-0.1.0-py3-none-any.whl

Download URL static_hosting_mcp-0.1.0-py3-none-any.whl
Size 33.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8922ac15f226a994785c12250d9d0e8e955e7262984a70e46f86cbd3d27104c8
BLAKE2b-256 checksum
How to use checksums
bf655cc1aad8a29920e0cee2ff8ed34177dc1a849def068f3c9a4712f1638b84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for static-hosting-mcp 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page