diff --git a/deploy/Dockerfile.trail-mcp b/deploy/Dockerfile.trail-mcp new file mode 100644 index 0000000..95997be --- /dev/null +++ b/deploy/Dockerfile.trail-mcp @@ -0,0 +1,38 @@ +# Trail MCP server WITH data-source providers (FMP / EDGAR / GMD) — serves the six tools over +# streamable-HTTP and can load LIVE data via {"config":"/config/trail.yaml"}. +# Build from the workspace root: docker build -f deploy/Dockerfile.trail-mcp -t /trail-mcp-full: . +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +# Local trail-lang (with the streamable-http transport). COPY only metadata + the package tree +# so the sibling .venv/.git never enter the build context. +COPY trail-py/pyproject.toml trail-py/README.md trail-py/ +COPY trail-py/trail trail-py/trail + +# Local provider adapters; their runtime deps (aiofmp, edgartools, …) resolve from PyPI. +COPY trail-fmp/pyproject.toml trail-fmp/README.md trail-fmp/ +COPY trail-fmp/trail_fmp trail-fmp/trail_fmp +COPY trail-edgar/pyproject.toml trail-edgar/README.md trail-edgar/ +COPY trail-edgar/trail_edgar trail-edgar/trail_edgar +COPY trail-gmd/pyproject.toml trail-gmd/README.md trail-gmd/ +COPY trail-gmd/trail_gmd trail-gmd/trail_gmd + +# Install trail-lang first (satisfies each adapter's trail-lang requirement from the local tree, +# not PyPI), then the three adapters. +RUN pip install "./trail-py[mcp]" ./trail-fmp ./trail-edgar ./trail-gmd + +# Non-root user; cache dirs the trail.yaml points at (FMP cache is host-mounted at /cache). +RUN useradd --create-home --uid 1000 trail \ + && mkdir -p /cache /edgar-cache /gmd-cache /config \ + && chown -R trail:trail /cache /edgar-cache /gmd-cache /config +USER trail + +EXPOSE 3000 +ENTRYPOINT ["trail", "mcp"] +CMD ["--transport", "streamable-http", "--host", "0.0.0.0", "--port", "3000"] diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..1ef8986 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,41 @@ +# Trail MCP — k3s deployment + +Serves the Trail MCP server (the six tools — `functions`, `schema`, `validate`, `describe`, `eval`, +`run`) over **streamable-HTTP** at `http://trail-mcp.ws.local/mcp`. dnsmasq resolves `*.ws.local` → +the Traefik LB, so the host routes automatically once the Ingress exists. + +## Two variants + +| File | Image | Data it can reach | +|---|---|---| +| `trail-mcp.yaml` (lean) | `trail-py/Dockerfile` (in the trail-lang repo) — `trail-lang[mcp]` only | inline `{"rows":[…]}` and `{"file":"…"}` | +| `trail-mcp-full.yaml` (live) | `Dockerfile.trail-mcp` (this dir) — adds the FMP/EDGAR/GMD providers | the above **plus** `{"config":"/config/trail.yaml"}` (live sources) | + +## Build the provider image + +Requires the trail repos checked out as siblings (`trail-py`, `trail-fmp`, `trail-edgar`, +`trail-gmd`) — the workspace layout. From the workspace root: + +```bash +docker build -f deploy/Dockerfile.trail-mcp -t 192.168.70.168:30500/trail-mcp-full: . +docker push 192.168.70.168:30500/trail-mcp-full: # k3s trusts this local registry +``` + +## Deploy + +```bash +# Credentials — created out-of-band, never committed: +kubectl -n trail create secret generic trail-mcp-creds \ + --from-literal=FMP_API_KEY="" \ + --from-literal=EDGAR_IDENTITY="Your Name you@example.com" + +kubectl apply -f deploy/trail-mcp-full.yaml # or trail-mcp.yaml for the lean variant +``` + +The full variant mounts the host FMP Parquet cache (`/home/.../aiofmp-cache/cache` → `/cache`, +single-node) and the trail.yaml ConfigMap at `/config/trail.yaml`. + +## Use + +Point an MCP client's **streamable-HTTP** transport at `http://trail-mcp.ws.local/mcp`. Tool `data` +argument is one of `{"rows":[…]}`, `{"file":"…"}`, or (full image) `{"config":"/config/trail.yaml"}`. diff --git a/deploy/trail-mcp-full.yaml b/deploy/trail-mcp-full.yaml new file mode 100644 index 0000000..6e002c1 --- /dev/null +++ b/deploy/trail-mcp-full.yaml @@ -0,0 +1,115 @@ +# Trail MCP with LIVE data sources (FMP / EDGAR / GMD) on k3s, at http://trail-mcp.ws.local/mcp. +# Supersedes deploy/trail-mcp.yaml (the lean, provider-less variant). +# +# Prereq — create the credentials Secret first (NOT committed): +# kubectl -n trail create secret generic trail-mcp-creds \ +# --from-literal=FMP_API_KEY="" \ +# --from-literal=EDGAR_IDENTITY="Your Name your.email@example.com" +# +# Then: kubectl apply -f deploy/trail-mcp-full.yaml +# Use from a tool call: eval/describe/run with data = {"config": "/config/trail.yaml"} +--- +apiVersion: v1 +kind: Namespace +metadata: { name: trail } +--- +apiVersion: v1 +kind: ConfigMap +metadata: { name: trail-mcp-config, namespace: trail } +data: + trail.yaml: | + # Credentials come from the environment (the trail-mcp-creds Secret); no secrets in this file. + sources: + edgar: + driver: edgar + options: + cache_dir: /edgar-cache + tickers: [AAPL, MSFT, NVDA, ORCL, CRM, GOOGL, META, DIS, NFLX, VZ, AMZN, HD, MCD, NKE, LOW, KO, PG, WMT, PEP, COST, JNJ, UNH, PFE, MRK, ABBV, XOM, CVX, COP, SLB, EOG, HON, UNP, CAT, GE, BA, JPM, BAC, WFC, GS] + fmp: + driver: fmp + options: + cached: true + cache_dir: /cache # host-mounted 2.5G Parquet cache + requests_per_minute: 300 + tickers: [AAPL, MSFT, NVDA, ORCL, CRM, GOOGL, META, DIS, NFLX, VZ, AMZN, HD, MCD, NKE, LOW, KO, PG, WMT, PEP, COST, JNJ, UNH, PFE, MRK, ABBV, XOM, CVX, COP, SLB, EOG, HON, UNP, CAT, GE, BA, JPM, BAC, WFC, GS] + gmd: + driver: gmd + options: + cache_dir: /gmd-cache + precedence: + income: [edgar, fmp] + balance: [edgar, fmp] + cash: [edgar, fmp] + default: [fmp, gmd] + panel: + periods: [2015, 2024] + pit: auto + strict: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trail-mcp + namespace: trail + labels: { app: trail-mcp } +spec: + replicas: 1 + selector: + matchLabels: { app: trail-mcp } + template: + metadata: + labels: { app: trail-mcp } + spec: + containers: + - name: trail-mcp + image: 192.168.70.168:30500/trail-mcp-full:0fa7d8e + imagePullPolicy: IfNotPresent + envFrom: + - secretRef: { name: trail-mcp-creds } # FMP_API_KEY, EDGAR_IDENTITY + ports: + - { containerPort: 3000, name: http } + volumeMounts: + - { name: config, mountPath: /config, readOnly: true } + - { name: fmp-cache, mountPath: /cache } + readinessProbe: + tcpSocket: { port: 3000 } + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + tcpSocket: { port: 3000 } + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: { cpu: 50m, memory: 256Mi } + limits: { memory: 1Gi } + volumes: + - name: config + configMap: { name: trail-mcp-config } + - name: fmp-cache + hostPath: + path: /home/usmanshahid/Documents/aiofmp-cache/cache # single-node k3s + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: { name: trail-mcp, namespace: trail } +spec: + selector: { app: trail-mcp } + ports: + - { port: 3000, targetPort: 3000, name: http } +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: { name: trail-mcp, namespace: trail } +spec: + ingressClassName: traefik + rules: + - host: trail-mcp.ws.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: trail-mcp + port: { number: 3000 } diff --git a/deploy/trail-mcp.yaml b/deploy/trail-mcp.yaml new file mode 100644 index 0000000..6517b5b --- /dev/null +++ b/deploy/trail-mcp.yaml @@ -0,0 +1,67 @@ +# Trail MCP server on k3s, exposed via Traefik at http://trail-mcp.ws.local/mcp +# (dnsmasq: *.ws.local -> 192.168.70.168, the Traefik LB). +apiVersion: v1 +kind: Namespace +metadata: + name: trail +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trail-mcp + namespace: trail + labels: { app: trail-mcp } +spec: + replicas: 1 + selector: + matchLabels: { app: trail-mcp } + template: + metadata: + labels: { app: trail-mcp } + spec: + containers: + - name: trail-mcp + image: 192.168.70.168:30500/trail-mcp:dac0ac2 + imagePullPolicy: IfNotPresent + # Image CMD already runs: trail mcp --transport streamable-http --host 0.0.0.0 --port 3000 + ports: + - { containerPort: 3000, name: http } + readinessProbe: + tcpSocket: { port: 3000 } # /mcp needs POST + Accept headers; TCP connect is the right liveness signal + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + tcpSocket: { port: 3000 } + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { memory: 512Mi } +--- +apiVersion: v1 +kind: Service +metadata: + name: trail-mcp + namespace: trail +spec: + selector: { app: trail-mcp } + ports: + - { port: 3000, targetPort: 3000, name: http } +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: trail-mcp + namespace: trail +spec: + ingressClassName: traefik + rules: + - host: trail-mcp.ws.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: trail-mcp + port: { number: 3000 }