Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions narratives/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ figma_*
screenshots
.gemini
.figma_pat
# Ignore stray Python scripts at the project root, but keep the agent
# sidecar's source (agent/ and its subpackages) tracked — it is part of the app.
*.py
!agent/**/*.py
sample.js

# Redundant snapshot of the original AI Studio scaffold
Expand Down
45 changes: 45 additions & 0 deletions narratives/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Sidecar agent for the Custom Data Commons multi-container Cloud Run service.
# Vendored from upstream additional_features/mcp_proxy_only.py (Apache-2.0),
# adapted for multi-instance Cloud Run.
#
# Built linux/amd64 only — Cloud Run rejects arm64 silently with a
# "failed to find supported architecture" startup error.

FROM --platform=linux/amd64 python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PROXY_PORT=5001 \
MCP_PORT=8082 \
TIMEZONE=UTC \
ALLOWED_ORIGIN=*

WORKDIR /app

# System deps: tzdata is needed for ZoneInfo on slim images.
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Run as a dedicated non-root user. /app is owned by that user so the config
# bootstrap can write config.json there at startup without root.
RUN groupadd -g 10001 appuser \
&& useradd -u 10001 -g appuser -m -s /sbin/nologin appuser

COPY requirements.txt ./
RUN pip install -r requirements.txt

COPY main.py ./
COPY src/ ./src/

RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 5001

# Cloud Run health probes hit the agent on PROXY_PORT; the agent is the
# sidecar container, so the ingress container's nginx reverse-proxies
# /agent/* to 127.0.0.1:${PROXY_PORT}.
CMD ["python", "-u", "main.py"]
75 changes: 75 additions & 0 deletions narratives/agent/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Build and (optionally) push the agent sidecar image to Artifact Registry.
#
# Tag is the short git SHA so image and infra (Terraform tfvars) stay in sync.

set -euo pipefail

usage() {
cat <<'USAGE'
Build and (optionally) push the agent sidecar image to Artifact Registry.

Usage: ./build.sh [--push] [--help]

Options:
--push Push the built image (both the SHA tag and :latest) to AR.
--help Show this help and exit.

Environment overrides (with defaults):
AR_REGION Artifact Registry region (us-central1)
AR_REPO Artifact Registry repo (custom-dc)
PROJECT GCP project (gdatacomms)
USAGE
}

# Resolve the script's own directory so docker build works regardless of the
# caller's current working directory.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

PUSH=false
for arg in "$@"; do
case "${arg}" in
--push) PUSH=true ;;
--help|-h) usage; exit 0 ;;
*)
echo "Error: unknown argument '${arg}'" >&2
usage >&2
exit 1
;;
esac
done

AR_REGION="${AR_REGION:-us-central1}"
AR_REPO="${AR_REPO:-custom-dc}"
PROJECT="${PROJECT:-gdatacomms}"
IMAGE_NAME="agent"
TAG="$(git rev-parse --short=12 HEAD)"

IMAGE_BASE="${AR_REGION}-docker.pkg.dev/${PROJECT}/${AR_REPO}/${IMAGE_NAME}"
FULL_IMAGE="${IMAGE_BASE}:${TAG}"
LATEST_IMAGE="${IMAGE_BASE}:latest"

echo "Building ${FULL_IMAGE}"
echo " PROJECT=${PROJECT} AR_REGION=${AR_REGION} AR_REPO=${AR_REPO}"

docker buildx build \
--platform linux/amd64 \
-t "${FULL_IMAGE}" \
-t "${LATEST_IMAGE}" \
--load \
"${DIR}"

# Verify amd64 — Cloud Run rejects arm64 silently.
ARCH="$(docker inspect --format '{{.Architecture}}' "${FULL_IMAGE}")"
if [[ "${ARCH}" != "amd64" ]]; then
echo "FATAL: image architecture is ${ARCH}, expected amd64" >&2
exit 1
fi
echo " arch verified: ${ARCH}"

if [[ "${PUSH}" == true ]]; then
echo "Pushing ${FULL_IMAGE}"
docker push "${FULL_IMAGE}"
docker push "${LATEST_IMAGE}"
echo "Done. Tag for tfvars: ${TAG}"
fi
69 changes: 69 additions & 0 deletions narratives/agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
MCP Proxy Server (Proxy-Only Mode)

This script provides a REST API with CORS for browser-based frontends
to communicate with an already running Data Commons MCP server.

Prerequisites:
Start the MCP server first:
python3 -m uv tool run datacommons-mcp serve http --port 3000

Usage:
python main.py
"""

from src.config import _bootstrap_config_from_url
from src.mcp.client import get_tools, initialize_mcp, MCP_PORT
from src.server.app import app, PROXY_PORT
from src.server.routes import register_all

register_all(app)


def main():
"""Main entry point."""
_bootstrap_config_from_url()

print("=" * 60)
print("Data Commons MCP Proxy Server (Proxy-Only Mode)")
print("=" * 60)
print(f"\nExpecting MCP server at: http://localhost:{MCP_PORT}")
print("\nMake sure you started the MCP server first:")
print(f" python3 -m uv tool run datacommons-mcp serve http --port {MCP_PORT}")

# Try to connect to MCP server
print("\nChecking MCP server connection...")
if initialize_mcp():
tools = get_tools()
print(f"\nConnected! Found {len(tools)} tools:")
for t in tools:
print(f" - {t.get('name')}")
else:
print("\nWARNING: Could not connect to MCP server")
print("The proxy will start anyway - MCP server can be started later")

# Start proxy
print(f"\nStarting proxy on port {PROXY_PORT}...")
print(f"Frontend should connect to: http://localhost:{PROXY_PORT}")
print("\nPress Ctrl+C to stop")
print("=" * 60)

app.run(host="0.0.0.0", port=PROXY_PORT, debug=False, threaded=True)


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions narratives/agent/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
annotated-types==0.7.0
anyio==4.12.0
blinker==1.9.0
cachetools==6.2.4
certifi==2025.11.12
charset-normalizer==3.4.4
click==8.3.1
distro==1.9.0
Flask==3.1.2
flask-cors==6.0.2
google-ai-generativelanguage==0.6.15
google-api-core==2.28.1
google-api-python-client==2.187.0
google-auth==2.45.0
google-auth-httplib2==0.3.0
google-cloud-secret-manager==2.20.2
google-genai==1.56.0
google-generativeai==0.8.6
googleapis-common-protos==1.72.0
grpcio==1.76.0
grpcio-status==1.71.2
h11==0.16.0
httpcore==1.0.9
httplib2==0.31.0
httpx==0.28.1
idna==3.11
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
proto-plus==1.27.0
protobuf==5.29.5
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.12.5
pydantic_core==2.41.5
pyparsing==3.2.5
requests==2.32.5
rsa==4.9.1
sniffio==1.3.1
tenacity==9.1.2
tqdm==4.67.1
typing-inspection==0.4.2
typing_extensions==4.15.0
uritemplate==4.2.0
urllib3==2.6.2
websockets==15.0.1
Werkzeug==3.1.4
Empty file.
Empty file.
Loading