Skip to content

A workshop project for the 2025 Snyk AI Security Summit. Build as Secure Hardened MCP Server.

ArcadeAI/snyk-mcp-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Building Secure MCP Servers

Snyk AI Security Summit Workshop
Breaking the Toxic Flow Triangle with Arcade MCP

Arcade MCP Snyk MCP

🎯 What You'll Build in 30 Minutes

Part 1: Secure MCP Server Framework

  • 5 production-quality tools
  • GitHub OAuth integration
  • Tool chaining with OAuth continuity
  • Security-first architecture

Part 2: Arcade Gateway

  • Access 1000+ production toolkits instantly
  • Google Calendar, Slack, Gmail, GitHub
  • Zero code, managed OAuth

Outcome: Production-ready MCP servers that eliminate factor #2 of the toxic flow triangle


⚠️ The Toxic Flow Triangle

         1️⃣ Untrusted Instructions
          (Prompt injection, jailbreaks)
                    β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚                   β”‚
    2️⃣ Sensitive Data   3️⃣ Exfil Path
     (API keys, OAuth)    (Logs, Caches,
                           LLM Memory)

     When all three combine β†’ TOXIC FLOW ☠️

Traditional MCP Tools: All 3 Factors Present ❌

# ❌ BAD: Traditional approach
def my_tool(api_key: str, repo: str) -> dict:
    # API key passed as parameter
    headers = {"Authorization": f"Bearer {api_key}"}
    # Token visible in protocol, logged, cached

Client calls:

{
  "tool": "my_tool",
  "args": {
    "api_key": "ghp_xxxxxxxxxxxx",  ← EXPOSED!
    "repo": "my-org/my-repo"
  }
}

Problems:

  • ✘ Factor #2: API key in protocol
  • ✘ Factor #3: Gets logged, cached, visible to LLM
  • ✘ Prompt injection can extract credentials
  • ✘ Not multi-tenant (same key for all users)

Arcade MCP: Factor #2 Eliminated βœ…

# βœ… GOOD: Arcade MCP approach
@app.tool(requires_auth=GitHub(scopes=["repo"]))
async def my_tool(context: Context, repo: str) -> dict:
    # OAuth token injected at runtime
    token = context.get_auth_token_or_empty()
    headers = {"Authorization": f"Bearer {token}"}
    # Token NEVER in protocol!

Client calls:

{
  "tool": "my_tool",
  "args": {
    "repo": "my-org/my-repo"  ← No API key!
  }
}

Benefits:

  • βœ“ Factor #2: Token stays server-side
  • βœ“ Factor #3: BROKEN - no sensitive data in protocol
  • βœ“ Can't exfiltrate what isn't there
  • βœ“ Multi-tenant: Each user gets their own token

πŸ—οΈ Architecture: How Arcade MCP Eliminates Toxic Flows

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                    MCP Client (Gemini CLI)                       ┃
┃                  "Fetch code from my-repo"                       ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
β”‚
MCP Protocol (HTTP/stdio)
JSON-RPC messages
βœ… NO CREDENTIALS HERE! βœ…
β”‚
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃         Arcade MCP Server (Your Tools)                        ┃
┃                                                               ┃
┃  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃
┃  β”‚           MCP Protocol Handler                           β”‚ ┃
┃  β”‚  β€’ Receives tool call request                            β”‚ ┃
┃  β”‚  β€’ NO credentials in request!                            β”‚ ┃
┃  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃
┃                           β”‚                                   ┃
┃  ┏━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓  ┃
┃  ┃  Context Injection Layer (THE MAGIC!)                   ┃  ┃
┃  ┃                                                         ┃  ┃
┃  ┃   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            ┃  ┃
┃  ┃   β”‚   Secrets    β”‚          β”‚ OAuth Tokens β”‚            ┃  ┃
┃  ┃   β”‚   (.env)     β”‚          β”‚   (Arcade    β”‚            ┃  ┃
┃  ┃   β”‚              β”‚          β”‚   Platform)  β”‚            ┃  ┃
┃  ┃   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜            ┃  ┃
┃  ┃          β”‚                         β”‚                    ┃  ┃
┃  ┃          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    ┃  ┃
┃  ┃                     β”‚                                   ┃  ┃
┃  ┃             β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                          ┃  ┃
┃  ┃             β”‚ Context Object β”‚                          ┃  ┃
┃  ┃             β”‚  β€’ user_id     β”‚                          ┃  ┃
┃  ┃             β”‚  β€’ session_id  β”‚                          ┃  ┃
┃  ┃             β”‚  β€’ secrets     β”‚ ◀─ Injected at runtime   ┃  ┃
┃  ┃             β”‚  β€’ auth tokens β”‚ ◀─ Injected at runtime   ┃  ┃
┃  ┃             β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          ┃  ┃
┃  ┗━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛  ┃
┃                          β”‚                                    ┃
┃  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃
┃  β”‚  Tool Execution (with injected context)                  β”‚ ┃
┃  β”‚  tool.execute(context) ◀─ Has secrets & OAuth!           β”‚ ┃
┃  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

╔═══════════════════════════════════════════════════════════════╗
β•‘  πŸ’‘ Credentials injected AFTER the protocol layer             β•‘
β•‘      β†’ LLM never sees them, can't leak them!                  β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═══════════════════════════════════════════════════════════════════════╗
β•‘  πŸ”‘ THE KEY POINT: The only way to solve this is with an              β•‘
β•‘                    Agnostic Third Party Layer                         β•‘
β•‘                                                                       β•‘
β•‘  Credentials MUST be injected between the protocol and execution,     β•‘
β•‘  never passed through the MCP protocol itself.                        β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

πŸ› οΈ Action Based Tools!

Why These Tools Are Different

Traditional API wrappers: Match REST endpoints 1:1, require LLMs to understand HTTP semantics

Arcade MCP tools: Intent-specific, LLM-friendly, secure by design

"LLMs care about INTENT ('get my calendar'), not API parameters (GET /calendar/v3/events?timeMin=...). Arcade tools are built for how LLMs think."

The Problem with Traditional Wrappers

# Traditional: Mirrors REST API
def github_get_file(owner, repo, path, ref, access_token):
    """
    GET /repos/{owner}/{repo}/contents/{path}
    Query params: ref (optional)
    Headers: Authorization: Bearer {access_token}
    """
    # LLM must:
    # - Know HTTP semantics
    # - Manage OAuth tokens
    # - Handle error codes
    # - Parse JSON responses

LLM usage:

> Get the README from octocat/Hello-World

LLM thinks: "I need owner='octocat', repo='Hello-World', path='README.md', and... wait, where's my access_token? User, can you provide your GitHub token?"

Problems:

  • LLM manages credentials (factor #2!)
  • LLM understands HTTP (cognitive overhead)
  • Error messages are HTTP codes
  • Not intent-specific

Arcade MCP: Intent-Specific Tools

# Arcade MCP: Intent-based
@app.tool(requires_auth=GitHub(scopes=["repo"]))
async def fetch_github_code(
    context: Context,
    repo: Annotated[str, "Repository name (owner/repo)"],
    file_path: Annotated[str, "File to fetch"]
) -> str:
    """Fetch code from a GitHub repository.
    
    OAuth token is injected automatically.
    LLM never sees or manages credentials.
    """
    token = context.get_auth_token_or_empty()
    # Platform handles OAuth, tool uses it

LLM usage:

> Get the README from octocat/Hello-World

LLM thinks: "I have a tool `fetch_github_code`. Intent matches. Args: repo='octocat/Hello-World', file_path='README.md'. Call it."

Benefits:

  • βœ“ LLM focuses on INTENT, not HTTP
  • βœ“ Platform manages credentials (factor #2 eliminated!)
  • βœ“ Type hints guide LLM (Annotated types)
  • βœ“ Structured errors (JSON, not HTTP codes)

This is the paradigm shift: Tools built for LLMs, not for humans calling REST APIs.


πŸš€ Workshop Setup (5 Minutes)

Step 1: Clone This Repository

git clone https://github.com/ArcadeAI/snyk-mcp-workshop
cd snyk-mcp-workshop
uv venv
source .venv/bin/activate

Step 2: Install Arcade Secure MCP Framework

# Install the Arcade Secure MCP Framework
uv tool install arcade-mcp

This gives you everything to build MCP servers with Arcade:

  • arcade new command for scaffolding
  • arcade login for OAuth management

Step 3: Install Dependencies

uv pip install httpx

Step 4: Authenticate

# Create Arcade account (one-time)
arcade login

Step 5: Create a new Secure MCP Server

arcade new server-name 
## i.e. arcade new snyk_workshop

Meet The 5 Tools

Special #1: Run the Server

# Set environment variable
export FILE_ACCESS_TOKEN="demo-file-access-token-2025"

# Start server with HTTP transport
python3 server.py http

You should see:

╔══════════════════════════════════════════════════════════════╗
β•‘  Snyk Security Workshop - MCP Server                         β•‘
β•‘  Transport: HTTP                                             β•‘
β•‘  Tools: 5 (greet, read_file, analyze, fetch, audit)          β•‘
β•‘  **Breaking the Toxic Flow Triangle!**                       β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Special #2: Connect Gemini CLI

# Add Using The CLI
gemini mcp add snykhttp -t http http://127.0.0.1:8000/mcp

OR edit file ~/.gemini/settings.json

{
  "mcpServers": {
    "snykhttp": {
      "httpUrl": "http://127.0.0.1:8000/mcp"
    }
  }
}

Then test in gemini-cli:

gemini
ctrl + t #lists tools 

Tool 1: greet - Connectivity Test

Intent: Test basic MCP connectivity

Code:

@app.tool
def greet(name: Annotated[str, "Name to greet"]) -> str:
    return f"Hello, {name}! Welcome to Snyk AI Security Summit!"

Test:

> use snykhttp.greet to say hello to Workshop Attendees

Why it matters: Simple, no auth, proves MCP protocol is working.


Tool 2: read_file - Secret Injection Pattern

Intent: Read a file with access control

Code:

@app.tool(requires_secrets=["FILE_ACCESS_TOKEN"])
def read_file(context: Context, path: str, max_bytes: int = 50000) -> dict:
    # Secret injected at runtime from .env
    token = context.get_secret("FILE_ACCESS_TOKEN")
    
    # Validate access (in production, check token against DB)
    # Read file with safety bounds
    
    return {
        "content": file_content,
        "note": f"Access validated with token (...{token[-4:]})"
    }

Test:

> use snykhttp.read_file to read examples/vulnerable_code.py

What happens (and why it's GOOD security):

Error: Tool 'snykhttp_ReadFile' cannot be executed over 
unauthenticated HTTP transport for security reasons. This tool requires 
end-user authorization or access to sensitive secrets.

See: https://docs.arcade.dev/en/home/compare-server-types

STOP. This is not a bug. This is EXCELLENT security! 🎯

Talk Track (during workshop):

"Look at that error. Arcade is refusing to run a tool with secrets over unauthenticated HTTP. This is security by design.

Why? Because HTTP without authentication is unprotected. If your server is running on localhost and someone else on your network knows the port, they could call tools that use secrets. Arcade prevents this.

This error is PROOF that Arcade takes security seriously. It won't let you accidentally expose secrets over insecure transport.

To use tools with secrets or OAuth locally, you have two options:

  1. Use stdio transport (process-isolated, secure)
  2. Deploy to Arcade Cloud (authenticated HTTPS)

Let me show you stdio..."

Demo with stdio:

# Stop HTTP server
# Start with stdio transport
python3 server.py stdio
# Configure Gemini CLI for stdio:
# Edit file ~/.gemini/settings.json
{
  "mcpServers": {
    "snykstdio": {
      "command": "/absolute/path/to/snyk-mcp-workshop/.venv/bin/python",
      "args": ["server.py", "stdio"],
      "cwd": "/absolute/path/to/snyk-mcp-workshop/",
      "env": {
        "FILE_ACCESS_TOKEN": "demo-token-2025"
      }
    }
  }
}
# Check Tools
gemini mcp list
#Restart after MCP check
gemini

# Now test again:
> use snykstdio.read_file to read examples/vulnerable_code.py

Now it works! Returns file content with "note": "Access validated with token (...2025)"

Toxic Flow Prevention:

  • Factor #2: Secret in .env, injected at runtime
  • LLM sees: "...2025" (last 4 chars only)
  • Full secret NEVER in MCP protocol
  • BONUS: Arcade enforces transport security (won't run over unprotected HTTP)

The Security Model:

  1. Secret stored in .env: FILE_ACCESS_TOKEN=demo-file-access-token-2025
  2. Tool decorated: @app.tool(requires_secrets=["FILE_ACCESS_TOKEN"])
  3. Arcade checks transport: HTTP unauth? β†’ Reject! stdio or HTTPS? β†’ Allow!
  4. At runtime: context.get_secret("FILE_ACCESS_TOKEN") retrieves it
  5. MCP protocol: {"tool": "read_file", "args": {"path": "..."}} ← No secret!

This is defense in depth: Not just runtime injection, but also transport validation!


Tool 3: analyze_code_security - Security Analysis

Intent: Find security vulnerabilities in code

Code:

@app.tool
async def analyze_code_security(context: Context, code: str) -> dict:
    await context.log.info("Analyzing code...")
    
    issues = []
    
    # Check for code injection
    if "eval(" in code:
        issues.append({
            "severity": "CRITICAL",
            "type": "Code Injection",
            "issue": "eval() usage detected"
        })
    
    # Check for unsafe deserialization
    if "pickle.loads(" in code:
        issues.append({
            "severity": "CRITICAL",
            "type": "Unsafe Deserialization",
            "issue": "pickle.loads() detected"
        })
    
    # + checks for os.system, SQL injection, hardcoded secrets, etc.
    
    return {
        "total_issues": len(issues),
        "severity_counts": {...},
        "issues": issues
    }

Test:

> use snykstdio.analyze_code_security to check:
import pickle
def process(data):
    obj = pickle.loads(data)
    eval(obj['cmd'])

Result:

{
  "total_issues": 2,
  "severity_counts": {"CRITICAL": 2},
  "issues": [
    {"severity": "CRITICAL", "type": "Unsafe Deserialization", "issue": "pickle.loads()"},
    {"severity": "CRITICAL", "type": "Code Injection", "issue": "eval()"}
  ],
  "recommendation": "❌ CRITICAL - Do not deploy"
}

Why it's LLM-friendly:

  • Intent-based: "analyze this code for security issues"
  • Not: "POST /api/v1/security/scan with headers X-API-Key..."
  • Structured output: JSON the LLM can reason about
  • Actionable: Includes remediation guidance

Tool 4: fetch_github_code - OAuth Injection

Intent: Get code from a GitHub repository

Code:

@app.tool(requires_auth=GitHub(scopes=["repo"]))
async def fetch_github_code(
    context: Context,
    owner: Annotated[str, "Repository owner"],
    repo: Annotated[str, "Repository name"],
    file_path: Annotated[str, "File path"]
) -> str:
    # OAuth token injected by Arcade platform
    token = context.get_auth_token_or_empty()
    
    # Proper GitHub API headers (following Arcade pattern)
    headers = {
        "Accept": "application/vnd.github.raw+json",
        "Authorization": f"Bearer {token}",
        "X-GitHub-Api-Version": "2022-11-28"
    }
    url = f"https://api.github.com/repos/{owner}/{repo}/contents/{file_path}"
    
    async with httpx.AsyncClient() as client:
        response = await client.get(url, headers=headers)
        response.raise_for_status()
        return response.text

Test:

> use snyk.fetch_github_code for the repo arcadeai/snyk-mcp-workshop/examples/hello_world.py

Toxic Flow Prevention:

  • Factor #2: GitHub OAuth token managed by Arcade
  • User Authorize With OAuth URL β†’ Arcade stores token
  • At runtime: Token injected via context.get_auth_token_or_empty()
  • MCP protocol: {"tool": "fetch_github_code", "args": {"repo": "..."} ← No token!

Multi-Tenant:

  • Alice calls tool β†’ Gets her GitHub token
  • Bob calls tool β†’ Gets his GitHub token
  • Same server, isolated credentials

Tool 5: security_audit_workflow - πŸ”₯ THE WOW MOMENT

Intent: Complete security audit (fetch code + analyze)

Code:

@app.tool(requires_auth=GitHub(scopes=["repo"]))
async def security_audit_workflow(
    context: Context,
    repo: Annotated[str, "GitHub repository"],
    file_path: Annotated[str, "File to audit"]
) -> dict:
    await context.log.info(f"πŸ” Starting audit for {repo}/{file_path}")
    
    # CHAIN 1: Fetch code from GitHub
    # Child tool INHERITS parent's GitHub OAuth!
    code_result = await context.tools.call_raw(
        "SnykSecurityServer.FetchGithubCode",
        {"repo": repo, "file_path": file_path}
    )
    
    # CHAIN 2: Analyze the fetched code
    analysis_result = await context.tools.call_raw(
        "SnykSecurityServer.AnalyzeCodeSecurity",
        {"code": code_result.value}
    )
    
    return {
        "repo": repo,
        "file": file_path,
        "security_analysis": analysis_result.value,
        "workflow": {
            "auth_flow": "GitHub OAuth shared across tool chain",
            "toxic_flow_prevention": "OAuth never in MCP protocol"
        }
    }

Test:

> use snyk.security_audit_workflow for the repo arcadeai/snyk-mcp-workshop/examples/hello_world.py

Watch the server logs:

INFO | πŸ” Starting security audit for octocat/Hello-World/README.md
INFO | Step 1: Fetching code from GitHub (using OAuth)...
INFO | Step 1 complete: Fetched 1234 characters
INFO | Step 2: Analyzing code for security vulnerabilities...
INFO | Step 2 complete: Found 0 potential issues
INFO | βœ… Security audit workflow complete!

OAuth Continuity - THE MAGIC:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Parent Tool: security_audit_workflow                   β”‚
β”‚  Has GitHub OAuth from @app.tool(requires_auth=GitHub())β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
         context.tools.call_raw("FetchGithubCode", ...)
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Child Tool: fetch_github_code                          β”‚
β”‚  INHERITS parent's GitHub OAuth token!                  β”‚
β”‚  Same token, no re-auth, secure propagation             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚ Returns code
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Child Tool: analyze_code_security                      β”‚
β”‚  Analyzes the fetched code (no auth needed)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚ Returns analysis
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Parent Tool: Combines results                          β”‚
β”‚  Returns comprehensive audit report                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

SAME GitHub token through 3 tools!
LLM never saw it in ANY MCP call!

Toxic Flow Prevention at Scale:

  • One OAuth token
  • Three tools (parent + 2 children)
  • Two GitHub API calls
  • ZERO appearances in MCP protocol

This is architectural security.

Why this is revolutionary:

  • Composable: Build complex workflows from simple tools
  • Secure: OAuth propagates, never exposes
  • LLM-friendly: "Audit this file from GitHub" (one intent, multi-step execution)
  • Not traditional APIs: LLM doesn't see OAuth flows, HTTP verbs, header management

πŸ”— Tool Chaining: Composable Security

Why Tool Chaining Matters

Without chaining: Each tool is isolated, LLM coordinates

LLM: Call fetch_code β†’ Get result β†’ Call analyze β†’ Get result β†’ Combine
     ↑ LLM has to manage state and coordinate

With chaining: Tools orchestrate, LLM gives intent

LLM: Call security_audit_workflow
Tool: Fetches code β†’ Analyzes β†’ Returns combined report
      ↑ Tool manages workflow, LLM just states intent

Benefits:

  • Simpler for LLM: One intent ("audit this file") vs multi-step coordination
  • Secure: OAuth flows through chain, LLM never sees it
  • Composable: Build complex workflows from simple building blocks
  • Atomic: Workflow succeeds or fails as a unit

How context.tools.call_raw() Works

# Parent tool
@app.tool(requires_auth=GitHub(scopes=["repo"]))
async def parent_tool(context: Context) -> dict:
    # Context has:
    # - context.user_id: "alice"
    # - context.session_id: "sess_123"
    # - context.authorization: {github_token}
    
    # Call child tool
    result = await context.tools.call_raw(
        "SnykSecurityServer.ChildTool",
        {"param": "value"}
    )
    
    # Child tool executed with SAME context:
    # - Same user_id: "alice"
    # - Same session_id: "sess_123"
    # - Same authorization: {github_token}
    
    return result.value

Key Insight: context propagates automatically. Child inherits parent's credentials, session, everything.


🌐 Part 2: Arcade Gateway - Instant Production Tools

What is Arcade Gateway?

A unified Secure MCP server exposing 1000+ production toolkits without writing code. The Gateway is your Centralized Zone for Security and Governance Management.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Arcade Gateway Architecture                  β”‚
β”‚                                                           β”‚
β”‚  Gemini CLI ────► Arcade Gateway ────► Toolkits           β”‚
β”‚                   (One endpoint)        β”‚                 β”‚
β”‚                                         β”œβ”€β–Ί Google        β”‚
β”‚                                         β”œβ”€β–Ί Slack         β”‚
β”‚                                         β”œβ”€β–Ί Gmail         β”‚
β”‚                                         β”œβ”€β–Ί GitHub        β”‚
β”‚                                         β”œβ”€β–Ί Notion        β”‚
β”‚                                         └─► 1k+ more      β”‚
β”‚                                                           β”‚
β”‚  Benefits:                                                β”‚
β”‚  βœ“ No code to write                                       β”‚
β”‚  βœ“ OAuth managed by Arcade                                β”‚
β”‚  βœ“ One security boundary                                  β”‚
β”‚  βœ“ Centralized governance                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Setup Steps

1. Create Gateway (5 minutes)

Visit dashboard.arcade.dev:

  1. Click "MCP Gateways" β†’ "Create Gateway"
  2. Name: Snyk Workshop Gateway
  3. Select Toolkits:
    • βœ… Google (Calendar, Gmail, Drive)
    • βœ… Slack (Channels, Messages)
    • βœ… 1000+ more available
  4. Save and copy:
    • Gateway slug (e.g., snyk-workshop-abc123)
  5. Create API Key

2. Configure Gemini CLI

Use Gemini CLI Commands:

gemini mcp add arcade -t http https://api.arcade.dev/mcp/YOUR-SLUG -H "Authorization: Bearer arc_YOUR_PROJECT_API_KEY" -H "Arcade-User-ID: your@email.com"

OR edit ~/.gemini/settings.json:

{
  "mcpServers": {
    "snykhttp": {
      "httpUrl": "http://127.0.0.1:8000/mcp"
    },
    "arcade": {
      "httpUrl": "https://api.arcade.dev/mcp/YOUR-SLUG",
      "headers": {
        "Authorization": "Bearer <YOUR_PROJECT_API_KEY>",
        "Arcade-User-ID": "<YOUR_EMAIL>"
      }
    }
  }
}

Replace:

  • YOUR-SLUG β†’ Your gateway slug
  • YOUR_PROJECT_API_KEY β†’ Your project API key
  • YOUR_EMAIL β†’ Your Arcade account email

3. Test Both Servers

gemini mcp list #You Should See 2 Servers

You'll see TWO servers:

MCP Servers:
1. snyk_security (5 tools)
   - greet
   - read_file
   - analyze_code_security
   - fetch_github_code
   - security_audit_workflow

2. arcade_gateway (Many+ tools)
   - Google.Calendar.ListEvents
   - Google.Calendar.CreateEvent
   - Slack.PostMessage
   - Gmail.SendEmail
   - GitHub.CreateIssue
   - ... and more

Test custom server:

> use snykhttp.analyze_code_security to check: import pickle; pickle.loads(data)

Test gateway:

> What emails did I get this morning? And what is on my calendar right now?

Why Use Both?

Custom Server (what you built):

  • βœ“ Custom business logic
  • βœ“ Security-specific tools
  • βœ“ Full control over implementation
  • βœ“ Your intellectual property

Arcade Gateway:

  • βœ“ Production toolkits instantly
  • βœ“ No code to maintain
  • βœ“ OAuth already handled
  • βœ“ Updates managed by Arcade

Together: Custom + Commodity = Complete Solution


πŸ” Escaped the Toxic Flow Triangle: Complete Analysis

Factor #1: Untrusted Instructions

What it is: Prompt injection, jailbreaks, malicious user input

How we mitigate:

  • βœ… Type validation: Annotated[str, "description"] guides LLM
  • βœ… Input bounds: max_bytes limits prevent DoS
  • βœ… Structured errors: Return JSON, not stack traces
  • βœ… Intent-based design: Tools match LLM reasoning patterns

Can we eliminate it? No. Users must interact with AI. But we validate.


Factor #2: Sensitive Data

What it is: API keys, OAuth tokens, database credentials

How we ELIMINATE it:

  • βœ… Secrets in .env: FILE_ACCESS_TOKEN stored outside code
  • βœ… OAuth via platform: @app.tool(requires_auth=GitHub) β†’ Arcade manages tokens
  • βœ… Runtime injection: context.get_secret(), context.get_auth_token_or_empty()
  • βœ… Never in protocol: MCP messages contain NO credentials

Can we eliminate it? YES! Credentials stay server-side. Protocol is clean.

This is the breakthrough: Factor #2 eliminated = Triangle broken.


Factor #3: Exfil Path

What it is: Logs, caches, LLM conversation memory, debug output

How we BREAK it:

  • βœ… No data to exfil: If factor #2 is eliminated, nothing sensitive in protocol
  • βœ… Logs are clean: Server logs don't echo secrets (we show last 4 chars only)
  • βœ… LLM memory clean: Conversation history has no credentials
  • βœ… Caches safe: MCP clients cache protocol messages, which are credential-free

Can we eliminate it? We don't need to! Without factor #2, there's nothing sensitive to exfiltrate.


Result: Architecture Prevents Toxic Flows

Traditional MCP:

1️⃣ Untrusted input + 2️⃣ Credentials in protocol + 3️⃣ Logs/caches = ☠️ TOXIC FLOW

Arcade MCP:

1️⃣ Untrusted input + ❌ (Factor #2 eliminated) + 3️⃣ Exfil path = βœ… SAFE
                        ↑
              No sensitive data in protocol
              = Nothing to exfiltrate

You can't exfiltrate what isn't in the protocol.

πŸ’‘ Key Takeaways

What You Learned

  1. Toxic Flow Triangle: 3 factors that combine to create AI security risks
  2. Architectural Security: Design to prevent, not just detect
  3. Runtime Injection: Secrets/OAuth injected at execution, never in protocol
  4. Tool Chaining: Composable workflows with secure context propagation
  5. Intent-Specific Tools: Built for LLMs, not traditional REST APIs
  6. Gateway Pattern: Custom tools + commodity integrations

Why This Matters

Before Arcade MCP:

  • Credentials hardcoded or passed as parameters
  • OAuth tokens visible to LLMs
  • Tools isolated, LLM coordinates
  • Factor #2 and #3 present β†’ Toxic flow risk

With Arcade MCP:

  • Credentials server-side only
  • Runtime injection via context
  • Tools chain with shared secure context
  • Factor #2 eliminated β†’ Triangle broken

Result: You can govern what you can observe, and you can't exfiltrate what isn't there.


Simple. Secure. Production-Ready.

πŸš€ Get Started | πŸ“– Read the Docs | πŸ’¬ Join Discord


Built with ❀️ @Arcade.dev for AI devs who care about security

About

A workshop project for the 2025 Snyk AI Security Summit. Build as Secure Hardened MCP Server.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages