Snyk AI Security Summit Workshop
Breaking the Toxic Flow Triangle with Arcade MCP
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
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 β οΈ
# β 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, cachedClient 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)
# β
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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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."
# 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 responsesLLM 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-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 itLLM 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.
git clone https://github.com/ArcadeAI/snyk-mcp-workshop
cd snyk-mcp-workshop
uv venv
source .venv/bin/activate# Install the Arcade Secure MCP Framework
uv tool install arcade-mcpThis gives you everything to build MCP servers with Arcade:
arcade newcommand for scaffoldingarcade loginfor OAuth management
uv pip install httpx# Create Arcade account (one-time)
arcade loginarcade new server-name
## i.e. arcade new snyk_workshop# Set environment variable
export FILE_ACCESS_TOKEN="demo-file-access-token-2025"
# Start server with HTTP transport
python3 server.py httpYou should see:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Snyk Security Workshop - MCP Server β
β Transport: HTTP β
β Tools: 5 (greet, read_file, analyze, fetch, audit) β
β **Breaking the Toxic Flow Triangle!** β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Add Using The CLI
gemini mcp add snykhttp -t http http://127.0.0.1:8000/mcpOR 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 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.
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:
- Use stdio transport (process-isolated, secure)
- 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.pyNow 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:
- Secret stored in
.env:FILE_ACCESS_TOKEN=demo-file-access-token-2025 - Tool decorated:
@app.tool(requires_secrets=["FILE_ACCESS_TOKEN"]) - Arcade checks transport: HTTP unauth? β Reject! stdio or HTTPS? β Allow!
- At runtime:
context.get_secret("FILE_ACCESS_TOKEN")retrieves it - MCP protocol:
{"tool": "read_file", "args": {"path": "..."}}β No secret!
This is defense in depth: Not just runtime injection, but also transport validation!
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
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.textTest:
> 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
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
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
# 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.valueKey Insight: context propagates automatically. Child inherits parent's credentials, session, everything.
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 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Visit dashboard.arcade.dev:
- Click "MCP Gateways" β "Create Gateway"
- Name:
Snyk Workshop Gateway - Select Toolkits:
- β Google (Calendar, Gmail, Drive)
- β Slack (Channels, Messages)
- β 1000+ more available
- Save and copy:
- Gateway slug (e.g.,
snyk-workshop-abc123)
- Gateway slug (e.g.,
- Create API Key
- Click Get API Key
- Create an API Key
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 slugYOUR_PROJECT_API_KEYβ Your project API keyYOUR_EMAILβ Your Arcade account email
gemini mcp list #You Should See 2 ServersYou'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?
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
What it is: Prompt injection, jailbreaks, malicious user input
How we mitigate:
- β
Type validation:
Annotated[str, "description"]guides LLM - β
Input bounds:
max_byteslimits 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.
What it is: API keys, OAuth tokens, database credentials
How we ELIMINATE it:
- β
Secrets in
.env:FILE_ACCESS_TOKENstored 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.
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.
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.
- Toxic Flow Triangle: 3 factors that combine to create AI security risks
- Architectural Security: Design to prevent, not just detect
- Runtime Injection: Secrets/OAuth injected at execution, never in protocol
- Tool Chaining: Composable workflows with secure context propagation
- Intent-Specific Tools: Built for LLMs, not traditional REST APIs
- Gateway Pattern: Custom tools + commodity integrations
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.
Built with β€οΈ @Arcade.dev for AI devs who care about security
