惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
GbyAI
GbyAI
C
Check Point Blog
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
Recent Announcements
Recent Announcements
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
J
Java Code Geeks
博客园_首页
Jina AI
Jina AI
美团技术团队
H
Help Net Security
MyScale Blog
MyScale Blog
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
S
SegmentFault 最新的问题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Bridging IFTTT to Your Local AI Assistant with an MCP Proxy
Maish Saidel-Keesing · 2026-06-18 · via DEV Community

So IFTTT shipped MCP support. That means you can control your automations, list applets, edit triggers, run queries... all through the Model Context Protocol. In theory, any MCP-capable AI assistant can now talk directly to IFTTT.

In practice? Not quite.

Right now, IFTTT officially supports only Claude and ChatGPT as AI assistant integrations. You go to Settings → Connectors in Claude, or Settings → Connected Apps in ChatGPT, and IFTTT is right there. But if your AI assistant isn't on that short list? You're on your own.

Why IFTTT's MCP Server Won't Talk to Your Local AI

Here's the situation. My AI assistant (Amazon Quick) speaks MCP via stdio. It launches a local process and communicates over stdin/stdout using JSON-RPC. Simple. Clean. Works great for local tools.

IFTTT's MCP server lives at https://ifttt.com/mcp and uses Streamable HTTP transport. It expects authenticated HTTP POST requests and responds with either JSON or Server-Sent Events streams.

Two completely different transport layers. They don't talk to each other.

So what do you do? You build a proxy.

Well... "you" build a proxy. In my case, I described the problem to Amazon Quick (my AI assistant) and it wrote the entire proxy for me. All ~500 lines of it.

I guided the architecture, debugged alongside it, and steered the fixes when things broke. But the actual code? That was all Quick guiding Kiro. This whole post is really about what happens when you pair an AI coding assistant with a well-defined integration problem.

What the Proxy Does

The proxy is a ~500-line Node.js script that sits between them:

┌────────────┐  stdio    ┌───────────┐  HTTPS  ┌──────────┐
│            │ JSON-RPC  │           │  POST   │          │
│   Amazon   │ ────────▶ │   MCP     │ ──────▶ │  IFTTT   │
│   Quick    │           │   Proxy   │         │  MCP     │
│            │ ◀──────── │  (Node)   │ ◀────── │ (Remote) │
│            │ JSON-RPC  │           │ SSE/JSON│          │
└────────────┘           └─────┬─────┘         └──────────┘
     local                     │                  remote
                        ┌──────┴──────┐
                        │ OAuth 2.1   │
                        │ PKCE + Auto │
                        │ Refresh     │
                        └─────────────┘

It reads JSON-RPC messages from stdin, forwards them as authenticated HTTPS requests to IFTTT, handles whatever response format comes back (direct JSON or SSE stream), and writes the response to stdout for Quick to consume.

The full flow:

  1. Authentication: OAuth 2.1 + PKCE (one-time browser flow)
  2. Token management: Auto-refresh when tokens expire
  3. Request proxying: stdin -> authenticated HTTPS POST to IFTTT
  4. Response handling: SSE streaming detection and parsing
  5. Response transformation: Format translation for client compatibility

Sounds straightforward? It mostly is. But two gotchas took me while to debug. Let me walk you through them.

How to Authenticate: OAuth 2.1 + PKCE

First things first. IFTTT requires OAuth authentication. The proxy has an --auth mode that handles the entire flow:

async function authenticate() {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);
  const state = generateState();

  const authParams = new URLSearchParams({
    client_id: CLIENT_ID,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    redirect_uri: REDIRECT_URI,
    resource: 'https://ifttt.com/mcp',
    response_type: 'code',
    scope: 'mcp',
    state: state,
  });

  // Opens browser, starts local callback server on port 3118
  // Exchanges code for token using PKCE verifier
  // Saves token to ~/.quickwork/ifttt-token.json
}

Run node index.js --auth once, authenticate in your browser, and the token gets saved locally. After that, the proxy handles refresh automatically. You never think about auth again.

The token management is simple but important:

function isTokenExpired(tokenData) {
  if (!tokenData || !tokenData.access_token) return true;
  if (!tokenData.expires_in) return false;
  const expiresAt = tokenData.obtained_at + (tokenData.expires_in * 1000);
  return Date.now() > expiresAt - 60000; // 1 minute buffer
}

That 60-second buffer matters. You don't want a request to fail because the token expires mid-flight.

Gotcha #1: Why IFTTT Returns Empty Responses

So here's where it got interesting.

My first version of the proxy was dead simple. Read from stdin, POST to IFTTT, buffer the response, write to stdout. Classic request/response.

It worked great for tools/list. IFTTT returned a nice 200 OK with a JSON body listing all available tools. I was feeling good.

Then I called my_applets.

Nothing came back. No error. No response. Just... silence.

After adding some debug logging, I discovered IFTTT was returning HTTP 202 Accepted with an empty body. The actual response? It was coming back as a Server-Sent Events stream. But my buffered HTTP client was already done. It saw the empty body, closed the connection, and moved on.

The fix is a streaming-aware HTTP client that checks the Content-Type header:

function httpsStreamingRequest(url, options, body, timeoutMs = 60000) {
  return new Promise((resolve, reject) => {
    const req = https.request(reqOptions, (res) => {
      const contentType = res.headers['content-type'] || '';
      const isSSE = contentType.includes('text/event-stream');

      if (isSSE) {
        // Keep the connection open, collect SSE events
        let sseBuffer = '';
        res.setEncoding('utf8');
        res.on('data', (chunk) => { sseBuffer += chunk; });

        res.on('end', () => {
          resolve({
            status: res.statusCode,
            isSSE: true,
            events: parseSSEBody(sseBuffer),
          });
        });
      } else {
        // Standard buffered response
        let data = '';
        res.on('data', (chunk) => { data += chunk; });
        res.on('end', () => {
          resolve({ status: res.statusCode, isSSE: false, body: data });
        });
      }
    });

    req.setTimeout(timeoutMs, () => {
      req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
    });

    if (body) req.write(body);
    req.end();
  });
}

The SSE parser itself is straightforward. Events are separated by double newlines, data lines start with data::

function parseSSEBody(body) {
  const events = [];
  const blocks = body.split('\n\n');

  for (const block of blocks) {
    let eventData = '';
    for (const line of block.split('\n')) {
      if (line.startsWith('data: ')) {
        eventData += line.substring(6);
      } else if (line.startsWith('data:')) {
        eventData += line.substring(5);
      }
    }
    if (eventData) {
      try { events.push(JSON.parse(eventData)); } catch (e) {}
    }
  }
  return events;
}

After this fix, my_applets worked beautifully. IFTTT returned 12 applets, all properly structured. I was back to feeling good.

For about 10 minutes.

Gotcha #2: Why Your Client Can't Read the Results

So the proxy was getting responses. IFTTT was sending back data. But Amazon Quick was still showing... nothing. Or more precisely, it was throwing a vague "Tool execution failed" error.

I pulled the raw JSON-RPC response to see what IFTTT was actually sending:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [],
    "isError": false,
    "structuredContent": {
      "applets": [...]
    }
  }
}

See it? The content array is empty. The actual data is in structuredContent.

According to the MCP spec, tool results go in the content array as TextContent or ImageContent objects. That's what Amazon Quick reads. IFTTT decided to put their data in a custom structuredContent field instead, leaving content as an empty array.

The fix is a response transformer that runs before writing to stdout:

function transformToolResponse(jsonRpcResponse) {
  if (!jsonRpcResponse || !jsonRpcResponse.result) return jsonRpcResponse;

  const result = jsonRpcResponse.result;

  if (
    result.structuredContent &&
    (!result.content || result.content.length === 0)
  ) {
    result.content = [
      {
        type: 'text',
        text: JSON.stringify(result.structuredContent, null, 2),
      },
    ];
  }

  return jsonRpcResponse;
}

12 lines. That's all it took. But finding the problem? That was the hard part.

The Main Proxy Loop

With both gotchas solved, the main proxy loop is clean:

async function proxyMcpRequest(jsonRpcMessage) {
  const token = await getValidToken();

  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
    'Accept': 'application/json, text/event-stream',
  };

  if (mcpSessionId) {
    headers['Mcp-Session-Id'] = mcpSessionId;
  }

  let response = await httpsStreamingRequest(IFTTT_MCP_URL, {
    method: 'POST', headers
  }, JSON.stringify(jsonRpcMessage));

  // Capture session ID for subsequent requests
  if (response.sessionId) {
    mcpSessionId = response.sessionId;
  }

  // Handle 401 - try token refresh
  if (response.status === 401) {
    cachedToken = await refreshToken(cachedToken);
    headers['Authorization'] = `Bearer ${cachedToken.access_token}`;
    response = await httpsStreamingRequest(IFTTT_MCP_URL, {
      method: 'POST', headers
    }, JSON.stringify(jsonRpcMessage));
  }

  return response;
}

The Accept: application/json, text/event-stream header is important. It tells IFTTT "I can handle both formats." Without it, you might not get the SSE stream at all.

How to Register It as an MCP Server

The proxy registers itself in the MCP config as a simple stdio server:

{
  "mcpServers": {
    "ifttt": {
      "command": "node",
      "args": ["/path/to/ifttt-mcp-proxy/index.js"]
    }
  }
}

That's it. Amazon Quick launches the process, pipes JSON-RPC to stdin, reads responses from stdout. The proxy handles everything in between: auth, streaming, format translation, token refresh.

What You Can Actually Do With It

With this proxy running, I can do all of this from my AI assistant using natural language:

  • "Show me my IFTTT applets" - lists all 12 applets with their triggers and actions
  • "What does the Create tweet with AI applet do?" - shows full configuration including the AI prompt
  • "Update the prompt on my tweet applet" - edits the applet configuration via API
  • "Disable the Reddit applet" - toggles applets on and off
  • "Create a new applet that..." - builds new automations from scratch

No browser. No IFTTT web UI. Just conversational access to my entire automation setup.

What I Learned Building This

A few takeaways if you're building something similar:

  1. The MCP spec has transport flexibility. Stdio and Streamable HTTP are both valid, but they don't interoperate automatically. If you're connecting a stdio client to an HTTP server, you need a proxy.
    If you're working with MCP on AWS, Amazon Bedrock Agents supports MCP servers natively for remote tool use... so you might not need a custom proxy if you're already in that ecosystem.

  2. SSE is sneaky. When a server returns 202 Accepted, your instinct is "okay, no content." But with SSE, the content is coming... just not the way you expect. Always check Content-Type before closing the connection.

  3. Not everyone implements the spec the same way. IFTTT's use of structuredContent instead of content[] is technically non-standard. Your proxy might need to normalize responses.

  4. OAuth 2.1 + PKCE is worth the complexity. No client secrets stored on disk, proper token rotation, and it works great for local tools that need to authenticate with remote services.

  5. AI assistants are shockingly good at integration plumbing. I didn't write a single line of this proxy by hand. I described the problem to Amazon Quick, and it generated the entire thing... the OAuth flow, the streaming HTTP client, the SSE parser, the response transformer.

When something broke, I described the symptoms and it diagnosed and fixed the issue. The whole thing went from "IFTTT has MCP support" to "fully working native integration" in about an hour of back-and-forth conversation. That's the real story here. I've written more about this dynamic between developer and AI coding assistant... it's a relationship worth understanding.
Tools like the AWS Toolkit for AI Agents are making this kind of AI-assisted building the norm rather than the exception.

The full proxy is about 500 lines of zero-dependency Node.js. No npm install needed. Just node and the built-in http, https, and crypto modules.

The complete source code is on GitHub.

I would be very interested to hear your thoughts or comments, so if you've built something similar or found a different approach, ping me on X or LinkedIn or feel free to leave a comment below.

And if you're trying to connect other remote MCP servers to a local client...
your mileage may vary, but the pattern should be the same.