Skip to content
Closed
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
38 changes: 38 additions & 0 deletions deploy/Dockerfile.trail-mcp
Original file line number Diff line number Diff line change
@@ -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 <reg>/trail-mcp-full:<tag> .
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"]
41 changes: 41 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -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:<tag> .
docker push 192.168.70.168:30500/trail-mcp-full:<tag> # 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="<your FMP 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"}`.
115 changes: 115 additions & 0 deletions deploy/trail-mcp-full.yaml
Original file line number Diff line number Diff line change
@@ -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="<your FMP 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 }
67 changes: 67 additions & 0 deletions deploy/trail-mcp.yaml
Original file line number Diff line number Diff line change
@@ -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 }
Loading