- Project Overview
- How It Works
- Architecture
- Prerequisites
- Project Structure
- Environment Setup
- Tailscale Configuration
- Docker Configuration
- MCP Server Details
- Running the Project
- Microsoft Copilot Studio Setup
- Testing
- Demo Guide
- Security
- Known Limitations
- Troubleshooting
This project implements an AI-driven fuel data translation pipeline. The core concept is simple:
A customer describes their fuel transaction data in plain English. An AI agent figures out how to map it to the EROAD API format, writes the conversion code, tests it, and deploys it. After that, the conversion runs automatically with no AI involved.
The AI does the hard thinking once. After the translation script is generated and validated, it runs in the Translation Engine with no AI involvement — making it fast, cheap, and deterministic.
AI generates script → deploys to engine → engine runs forever without AI
Without this system, every new customer data format requires a developer to manually write a translation script. With this system, a user can describe their data in plain language and the AI handles the rest.
1. User describes their data in plain English to the Copilot agent
2. Agent calls MCP #1 (EROAD) to understand:
- What fields EROAD requires
- What format each field must be in
- What fuel codes are valid
3. Agent calls MCP #2 (Translator) to understand:
- What conversion functions are available
- How to handle date formats, timezones, type coercion
4. Agent generates a JavaScript translation script
5. Agent pushes the script to the Translation Engine via MCP #2
6. Agent tests the script with a sample record
7. Agent validates the output against the EROAD schema via MCP #1
8. Agent submits a test transaction to confirm end-to-end flow
9. Done — the script is deployed and runs without AI from this point
Customer data → Translation Engine → EROAD-compatible payload → EROAD API
┌─────────────────────────────────────────────────────────┐
│ Microsoft Copilot Studio │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Copilot Agent │ │
│ │ Instructions: map customer data to EROAD format │ │
│ └──────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ MCP #1 │ │ MCP #2 │ │
│ │ EROAD API │ │ Translation │ │
│ │ (port 8000)│ │ Engine │ │
│ │ │ │ (port 8001) │ │
│ └──────┬──────┘ └────────┬─────────┘ │
└──────────┼─────────────────────┼──────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ Docker Containers │
│ │
│ ┌──────────────────────────┐ │
│ │ ts-mcp-eroad (Tailscale)│ │
│ │ mcp-eroad (Node.js) │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ ts-mcp-translator │ │
│ │ mcp-translator (Node.js)│ │
│ └──────────────────────────┘ │
└─────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ Tailscale Funnel │
│ │
│ mcp-eroad.tail11fa6e.ts.net │
│ mcp-translator.tail11fa6e.ts.net│
└─────────────────────────────────┘
| Software | Version | Download |
|---|---|---|
| Docker Desktop (macOS) | Latest | https://www.docker.com/products/docker-desktop |
| Tailscale | Latest | https://tailscale.com/download/mac |
| Node.js | 18+ | https://nodejs.org |
| Account | Purpose | URL |
|---|---|---|
| Tailscale | VPN and public endpoint exposure | https://tailscale.com |
| Microsoft | Copilot Studio access | https://copilotstudio.microsoft.com |
On macOS, Docker containers cannot use Linux kernel-level TUN
devices. Tailscale must run in userspace mode. This is handled
automatically by the TS_USERSPACE=true environment variable
in the Docker Compose configuration.
Do NOT add the following to your Docker Compose on macOS:
devices: /dev/net/tun— Linux onlycap_add: net_admin— Linux onlycap_add: sys_module— Linux only
fuel-translation-pipeline/
│
├── .env # Auth keys and secrets (never commit)
├── .gitignore # Git ignore rules
├── docker-compose.yml # Docker container definitions
├── README.md # This file
│
├── mcp-eroad/ # MCP #1 — EROAD Fuel API Server
│ ├── Dockerfile # Container build instructions
│ ├── package.json # Node.js dependencies
│ └── server.js # MCP server code
│
├── mcp-translator/ # MCP #2 — Translation Engine Server
│ ├── Dockerfile # Container build instructions
│ ├── package.json # Node.js dependencies
│ └── server.js # MCP server code
│
├── tailscale-eroad/ # Tailscale config for EROAD MCP
│ ├── state/ # Tailscale state (never commit)
│ ├── config/
│ │ └── serve.json # Tailscale serve configuration
│ └── start.sh # Auto-funnel startup script
│
└── tailscale-translator/ # Tailscale config for Translator MCP
├── state/ # Tailscale state (never commit)
├── config/
│ └── serve.json # Tailscale serve configuration
└── start.sh # Auto-funnel startup script
git clone <your-repo-url>
cd fuel-translation-pipelinecp .env.example .envEdit .env and fill in your values:
# Tailscale Auth Keys
# Generate these at https://login.tailscale.com/admin/settings/keys
# Settings: Reusable=Yes, Ephemeral=No, Pre-approved=Yes
TS_AUTHKEY_EROAD=tskey-auth-your-eroad-key-here
TS_AUTHKEY_TRANSLATOR=tskey-auth-your-translator-key-here
# EROAD API Key
# Leave blank for mock mode
# Fill in when connecting to real EROAD API
# EROAD_API_KEY=your-real-api-key-here- Go to
https://login.tailscale.com/admin/settings/keys - Click Generate auth key
- For the EROAD key:
- Description:
mcp-eroad - Reusable: Yes
- Ephemeral: No
- Pre-approved: Yes
- Tags:
tag:mcp-eroad
- Description:
- Copy the key and paste into
.env - Repeat for the translator key with tag
tag:mcp-translator
⚠️ You only see the key once. Copy it immediately.
⚠️ Pre-approved must be ticked otherwise containers will not join the tailnet automatically.
Go to https://login.tailscale.com/admin/acls and apply this policy:
{
"tagOwners": {
"tag:mcp-eroad": ["autogroup:admin"],
"tag:mcp-translator": ["autogroup:admin"],
"tag:mcp-host": ["autogroup:admin"]
},
"grants": [
{
"src": ["tag:mcp-host"],
"dst": ["tag:mcp-eroad"],
"ip": ["tcp:8000", "tcp:443"]
},
{
"src": ["tag:mcp-host"],
"dst": ["tag:mcp-translator"],
"ip": ["tcp:8001", "tcp:443"]
},
{
"src": ["autogroup:admin"],
"dst": ["tag:mcp-eroad", "tag:mcp-translator"],
"ip": ["*"]
},
{
"src": ["tag:mcp-eroad"],
"dst": ["tag:mcp-translator"],
"ip": ["tcp:8001", "tcp:443"]
},
{
"src": ["tag:mcp-translator"],
"dst": ["tag:mcp-eroad"],
"ip": ["tcp:8000", "tcp:443"]
},
{
"src": ["autogroup:member"],
"dst": ["tag:mcp-eroad", "tag:mcp-translator"],
"ip": ["*"]
}
],
"ssh": [
{
"action": "check",
"src": ["autogroup:admin"],
"dst": ["tag:mcp-eroad", "tag:mcp-translator"],
"users": ["autogroup:nonroot", "root"]
}
],
"nodeAttrs": [
{
"target": ["tag:mcp-eroad", "tag:mcp-translator"],
"attr": ["funnel"]
}
],
"tests": [
{
"src": "tag:mcp-host",
"accept": ["tag:mcp-eroad:8000"]
},
{
"src": "tag:mcp-host",
"accept": ["tag:mcp-eroad:443"]
},
{
"src": "tag:mcp-host",
"accept": ["tag:mcp-translator:8001"]
},
{
"src": "tag:mcp-host",
"accept": ["tag:mcp-translator:443"]
},
{
"src": "tag:mcp-eroad",
"deny": ["tag:mcp-host:8000"]
},
{
"src": "tag:mcp-eroad",
"deny": ["tag:mcp-host:8001"]
}
]
}sudo tailscale login --advertise-tags=tag:mcp-host- Go to
https://login.tailscale.com/admin/dns - Enable MagicDNS
- Enable HTTPS
version: "3.7"
services:
# ── Tailscale sidecar for MCP #1 (EROAD) ───────────────────────────────
ts-mcp-eroad:
image: tailscale/tailscale:latest
hostname: mcp-eroad
environment:
- TS_AUTHKEY=${TS_AUTHKEY_EROAD}
- TS_EXTRA_ARGS=--advertise-tags=tag:mcp-eroad
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=true
volumes:
- ${PWD}/tailscale-eroad/state:/var/lib/tailscale
- ${PWD}/tailscale-eroad/start.sh:/start.sh
entrypoint: >
/bin/sh -c "tailscaled --tun=userspace-networking &
sleep 5 && sh /start.sh & wait"
restart: unless-stopped
healthcheck:
test: ["CMD", "tailscale", "status"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
# ── MCP #1 — EROAD API ─────────────────────────────────────────────────
mcp-eroad:
build: ./mcp-eroad
network_mode: service:ts-mcp-eroad
depends_on:
ts-mcp-eroad:
condition: service_healthy
restart: unless-stopped
# ── Tailscale sidecar for MCP #2 (Translator) ──────────────────────────
ts-mcp-translator:
image: tailscale/tailscale:latest
hostname: mcp-translator
environment:
- TS_AUTHKEY=${TS_AUTHKEY_TRANSLATOR}
- TS_EXTRA_ARGS=--advertise-tags=tag:mcp-translator
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=true
volumes:
- ${PWD}/tailscale-translator/state:/var/lib/tailscale
- ${PWD}/tailscale-translator/start.sh:/start.sh
entrypoint: >
/bin/sh -c "tailscaled --tun=userspace-networking &
sleep 5 && sh /start.sh & wait"
restart: unless-stopped
healthcheck:
test: ["CMD", "tailscale", "status"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
# ── MCP #2 — Translation Engine ────────────────────────────────────────
mcp-translator:
build: ./mcp-translator
network_mode: service:ts-mcp-translator
depends_on:
ts-mcp-translator:
condition: service_healthy
restart: unless-stoppedBoth MCP servers use the same Dockerfile pattern:
mcp-eroad/Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD ["node", "server.js"]mcp-translator/Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8001
CMD ["node", "server.js"]Both servers use the same dependencies:
{
"name": "mcp-server",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.18.2"
}
}This server gives the AI agent awareness of the EROAD target schema. It runs in mock mode — no real EROAD API calls are made. Transactions are stored in memory.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Server health check |
| GET | /schema |
EROAD field definitions and constraints |
| GET | /fuelcodes |
Valid fuel product codes |
| POST | /validate |
Validate a payload against EROAD schema |
| POST | /fuel |
Submit a mock fuel transaction |
| GET | /fuel/transactions |
View all stored mock transactions |
| GET | /fuel/transactions/:id |
View a single transaction |
| DELETE | /fuel/transactions |
Clear all mock transactions |
| Method | Endpoint | Description |
|---|---|---|
| GET | /mcp |
Server discovery |
| POST | /mcp |
MCP Streamable HTTP protocol handler |
| GET | /.well-known/mcp |
Well-known discovery |
| Tool | Description |
|---|---|
get_schema |
Get EROAD field definitions |
get_fuel_codes |
Get valid product codes |
validate_payload |
Validate a transaction payload |
submit_transaction |
Submit to mock EROAD API |
get_transactions |
View stored transactions |
| Field | Type | Rules |
|---|---|---|
registrationPlate |
string | Uppercase, no spaces, e.g. ABC123 |
registrationState |
string | Default NZ |
transactionTimestamp |
datetime | UTC ISO 8601 |
amount |
decimal | Total including GST |
transactionType |
string | PURCHASE or VOID |
fuelProducts |
array | At least one entry required |
fuelProducts[].productCode |
string | DIESEL, PETROL_REGULAR, PETROL_PREMIUM |
fuelProducts[].quantity |
decimal | Volume in litres |
fuelProducts[].amount |
decimal | Cost of product |
This server accepts translation scripts from the AI agent, stores them, and executes them against incoming source data.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Server health check |
| GET | /patterns |
Available conversion patterns |
| POST | /translation/push |
Store a new translation script |
| POST | /translation/test |
Test the current script |
| GET | /translation/status |
Get current active script |
| POST | /translation/execute |
Execute script on real data |
| Method | Endpoint | Description |
|---|---|---|
| GET | /mcp |
Server discovery |
| POST | /mcp |
MCP Streamable HTTP protocol handler |
| GET | /.well-known/mcp |
Well-known discovery |
| Tool | Description |
|---|---|
get_patterns |
Get available conversion patterns |
push_script |
Push a translation script |
test_translation |
Test with a sample record |
get_status |
Get active script |
execute_translation |
Run on real data |
| Pattern | Description | Example |
|---|---|---|
toUpperCase |
Convert to uppercase | input.toUpperCase() |
stripSpaces |
Remove whitespace | input.replace(/\s/g, '') |
formatPlate |
Format NZ plate | input.toUpperCase().replace(/\s/g, '') |
parseDecimal |
String to decimal | parseFloat(input) |
removeGST |
Remove NZ GST 15% | parseFloat(input) / 1.15 |
nztToUtc |
NZT to UTC | See server.js for full implementation |
cd fuel-translation-pipeline
docker compose up -dWait about 30 seconds for Tailscale to authenticate and the servers to start. The startup scripts automatically configure Tailscale Funnel.
# Check container logs
docker compose logs --tail=10
# Check EROAD MCP is responding
curl https://mcp-eroad.tail11fa6e.ts.net/health
# Check Translator MCP is responding
curl https://mcp-translator.tail11fa6e.ts.net/health
# Check funnel status
docker exec -it fuel-translation-pipeline-ts-mcp-eroad-1 \
tailscale funnel status
docker exec -it fuel-translation-pipeline-ts-mcp-translator-1 \
tailscale funnel statusdocker compose downdocker compose down
docker compose build --no-cache
docker compose up -d# START
cd fuel-translation-pipeline && docker compose up -d
# VERIFY
curl https://mcp-eroad.tail11fa6e.ts.net/health
curl https://mcp-translator.tail11fa6e.ts.net/health
# STOP
docker compose down
# REBUILD
docker compose down && docker compose build --no-cache && docker compose up -d
# CLEAR DEMO DATA
curl -X DELETE https://mcp-eroad.tail11fa6e.ts.net/fuel/transactions
# VIEW LOGS
docker compose logs -f- Microsoft work account with Copilot Studio access
- Both MCP servers running and publicly accessible
- Generative Orchestration enabled in your agent
- Go to
https://copilotstudio.microsoft.com - Click Create → New agent
- Name it
Fuel Translation Agent - Click Create
- Click Settings → Generative AI
- Set Generative mode to On
- Click Save
- Go to Tools → Add a tool → New tool
- Select Model Context Protocol
- Enter URL:
https://mcp-eroad.tail11fa6e.ts.net/mcp
- Click Next — tools will be discovered automatically
- Click Save
Repeat the same process with:
https://mcp-translator.tail11fa6e.ts.net/mcp
In the agent Instructions field paste:
You are a fuel data translation assistant for EROAD.
When a user describes their source data model you will:
1. Use the EROAD MCP to understand the target schema
- Call get_schema to get field definitions
- Call get_fuel_codes to get valid fuel product codes
2. Use the Translation MCP to get available conversion patterns
- Call get_patterns to see what conversions are available
3. Generate a JavaScript translation function called translate(input)
that maps the user fields to the EROAD schema handling all
conversions including:
- Registration plate to uppercase with no spaces
- NZT datetime to UTC
- String amounts to decimals
- GST inclusive amounts
4. Push the script via push_script
5. Test the script via test_translation with a sample record
6. Validate the output via validate_payload on the EROAD MCP
7. If validation passes submit a test transaction via submit_transaction
8. Show the user the transaction ID and confirm success
Always ensure the final output has:
- registrationPlate uppercase with no spaces
- transactionTimestamp in UTC ISO 8601
- amount as a decimal number
- transactionType as PURCHASE or VOID
- fuelProducts array with at least one entry
This server uses Streamable HTTP transport as required by
Copilot Studio. The protocol version is 2024-11-05.
The key header that Copilot Studio looks for is:
x-ms-agentic-protocol: mcp-streamable-1.0
This is set automatically on all /mcp responses.
# Test initialize
curl -X POST https://mcp-eroad.tail11fa6e.ts.net/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}
}
}'
# Test tools list
curl -X POST https://mcp-eroad.tail11fa6e.ts.net/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# Test tool call
curl -X POST https://mcp-eroad.tail11fa6e.ts.net/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_schema",
"arguments": {}
}
}'# Push a test script
curl -X POST https://mcp-translator.tail11fa6e.ts.net/translation/push \
-H "Content-Type: application/json" \
-d '{
"script": "function translate(input) { return { registrationPlate: input.reg.toUpperCase().replace(/\\s/g, \"\"), registrationState: \"NZ\", transactionTimestamp: new Date(new Date(input.purchase_time).getTime() - 13*60*60*1000).toISOString(), amount: parseFloat(input.amount), transactionType: \"PURCHASE\", fuelProducts: [{ productCode: \"DIESEL\", quantity: parseFloat(input.litres), amount: parseFloat(input.amount) }] }; }"
}'
# Test the script
curl -X POST https://mcp-translator.tail11fa6e.ts.net/translation/test \
-H "Content-Type: application/json" \
-d '{
"reg": "abc 123",
"purchase_time": "2026-03-19T10:34:10",
"amount": "115.00",
"litres": "85.5"
}'
# Execute against real data (no AI)
curl -X POST https://mcp-translator.tail11fa6e.ts.net/translation/execute \
-H "Content-Type: application/json" \
-d '{
"reg": "xyz 789",
"purchase_time": "2026-03-19T14:22:00",
"amount": "200.00",
"litres": "150.0"
}'# Submit a transaction
curl -X POST https://mcp-eroad.tail11fa6e.ts.net/fuel \
-H "Content-Type: application/json" \
-d '{
"registrationPlate": "ABC123",
"registrationState": "NZ",
"transactionTimestamp": "2026-03-18T21:34:10.000Z",
"amount": 115.00,
"transactionType": "PURCHASE",
"fuelProducts": [{
"productCode": "DIESEL",
"quantity": 85.5,
"amount": 115.00
}]
}'
# View stored transactions
curl https://mcp-eroad.tail11fa6e.ts.net/fuel/transactions
# Clear transactions
curl -X DELETE https://mcp-eroad.tail11fa6e.ts.net/fuel/transactions# Start everything
docker compose up -d
# Wait 30 seconds then verify
curl https://mcp-eroad.tail11fa6e.ts.net/health
curl https://mcp-translator.tail11fa6e.ts.net/health
# Clear any previous demo data
curl -X DELETE https://mcp-eroad.tail11fa6e.ts.net/fuel/transactionsPaste this into your Copilot agent:
I have fuel transaction data I need to load into EROAD.
My data has the following fields:
- "reg" — an NZTA vehicle registration plate,
for example "abc 123"
- "purchase_time" — the date and time the fuel was
purchased in New Zealand time, could be in either
ISO format like "2026-03-19T10:34:10" or human
readable like "Mar 19th 2026 10:34:10 am"
- "amount" — the total dollar amount paid at the pump
including GST, for example "115.00"
- "litres" — the volume of fuel purchased in litres,
for example "85.5"
- "fuel_type" — always "diesel"
Please create a translation mapping for my data and
test it with this sample record:
{
"reg": "abc 123",
"purchase_time": "2026-03-19T10:34:10",
"amount": "115.00",
"litres": "85.5",
"fuel_type": "diesel"
}
✅ Agent calls get_schema from EROAD MCP
✅ Agent calls get_patterns from Translator MCP
✅ Agent generates a translate(input) function
✅ Agent calls push_script to deploy it
✅ Agent calls test_translation with the sample record
✅ Agent calls validate_payload to verify the output
✅ Agent calls submit_transaction and gets a MOCK ID
✅ Transaction appears in /fuel/transactions
curl https://mcp-eroad.tail11fa6e.ts.net/fuel/transactionsShow that the mock EROAD database contains exactly what would have been sent to the real EROAD API.
curl -X POST \
https://mcp-translator.tail11fa6e.ts.net/translation/execute \
-H "Content-Type: application/json" \
-d '{
"reg": "DEF 456",
"purchase_time": "2026-03-19T14:22:00",
"amount": "200.00",
"litres": "150.0",
"fuel_type": "diesel"
}'This runs the translation script directly with no AI involved — demonstrating the key design principle.
| Layer | Mechanism | Notes |
|---|---|---|
| Network | Tailscale VPN | Peer-to-peer encrypted |
| Public exposure | Tailscale Funnel | HTTPS only via Let's Encrypt |
| ACL | Tailscale ACL | Restricts which nodes can communicate |
| Application | IP allowlist middleware | Blocks non-Microsoft, non-Tailscale IPs |
| Data | Mock mode only | No real credentials or customer data |
| Protected | Not Protected |
|---|---|
| App ports 8000 and 8001 from public | MCP endpoints accessible via HTTPS |
| Direct container access | No auth on MCP endpoints themselves |
| Non-Tailscale traffic to containers | Translation scripts stored in memory only |
The Node.js servers include middleware that restricts access to:
- Localhost (
127.0.0.1) - Tailscale network (
100.x.x.xrange) - Tailscale IPv6 (
fd7a:115c:a1e0range) - Microsoft Azure IP ranges used by Copilot Studio
⚠️ This setup is for experimental and demo purposes only.
For production the following changes are recommended:
- Move off Tailscale Funnel to Azure networking
- Add API key authentication to MCP endpoints
- Add persistent storage for translation scripts
- Add rate limiting to prevent abuse
- Add request logging and monitoring
- Connect to the real EROAD API with proper key management
- Use Azure Key Vault for secret storage
| Limitation | Impact | Recommended Fix |
|---|---|---|
| Tailscale Funnel not production grade | Cannot use for real customers | Migrate to Azure networking |
| Translation scripts stored in memory | Lost on container restart | Add database persistence |
| No authentication on MCP endpoints | Anyone with URL can call them | Add API key validation |
| Funnel config lost on restart | Startup script re-creates it | Working as designed |
| Mock EROAD API only | Cannot submit real transactions | Add real EROAD API key |
| macOS only tested | Unknown behaviour on other OS | Test on Linux/Windows |
# Check Docker Desktop is running
docker info
# Check container status
docker compose ps
# View all logs
docker compose logs# Check auth key is valid
docker compose logs ts-mcp-eroad | grep "invalid key"
# Fix: regenerate auth keys at
# https://login.tailscale.com/admin/settings/keys
# Make sure Pre-approved is ticked# Check funnel status
docker exec -it fuel-translation-pipeline-ts-mcp-eroad-1 \
tailscale funnel status
# If not active, set it manually
docker exec -it fuel-translation-pipeline-ts-mcp-eroad-1 \
tailscale funnel --bg 8000
docker exec -it fuel-translation-pipeline-ts-mcp-translator-1 \
tailscale funnel --bg 8001# Check the MCP server is actually running
docker compose logs mcp-eroad | tail -5
docker compose logs mcp-translator | tail -5
# Should see:
# MCP #1 EROAD server running on port 8000
# MCP #2 Translation Engine running on port 8001- Verify the MCP endpoint responds correctly:
curl -X POST https://mcp-eroad.tail11fa6e.ts.net/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'- Check Generative mode is enabled in Copilot Studio
- Try removing and re-adding the MCP tool
- Check the URL has no trailing slash issues
# Check what script is currently active
curl https://mcp-translator.tail11fa6e.ts.net/translation/status
# Clear and let the agent regenerate
# (restart the container to clear in-memory script)
docker compose restart mcp-translatorDrop: TCP{...} no rules matched
This is normal — it is Tailscale internal peer-to-peer traffic. If your curl commands are working, ignore these logs. They do not indicate a problem with the Funnel setup.
| Service | URL |
|---|---|
| EROAD MCP | https://mcp-eroad.tail11fa6e.ts.net |
| Translation MCP | https://mcp-translator.tail11fa6e.ts.net |
| EROAD Health | https://mcp-eroad.tail11fa6e.ts.net/health |
| Translator Health | https://mcp-translator.tail11fa6e.ts.net/health |
| Mock Transactions | https://mcp-eroad.tail11fa6e.ts.net/fuel/transactions |
Both servers implement the MCP protocol version 2024-11-05
using Streamable HTTP transport as required by Copilot Studio.
Supported methods:
initializenotifications/initializedtools/listtools/call
Required response header:
x-ms-agentic-protocol: mcp-streamable-1.0
Documentation prepared by Johnson Zhang, March 2026
Based on mentoring sessions with Sean Ogden
EROAD Fuel Integration — Experimental Phase