Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
67 changes: 67 additions & 0 deletions narratives/agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
# Copyright 2024 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
import src.server.routes # noqa: F401 (registers routes)


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