{"slug": "bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy", "title": "Bridging IFTTT to Your Local AI Assistant with an MCP Proxy", "summary": "Amazon Quick, an AI assistant, gained the ability to control IFTTT automations through a custom proxy built by a developer. The proxy bridges the gap between Quick's stdio-based MCP transport and IFTTT's Streamable HTTP transport, handling OAuth authentication and response format conversion. The developer guided the architecture while Quick wrote the 500-line Node.js proxy code.", "body_md": "So IFTTT shipped [MCP support](https://ifttt.com/mcp). 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.\n\nIn practice? Not quite.\n\nRight now, IFTTT [officially supports](https://help.ifttt.com/hc/en-us/articles/47690989390619-Using-IFTTT-with-AI-Assistants) 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.\n\nHere's the situation. My AI assistant ([Amazon Quick](https://aws.amazon.com/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.\n\nIFTTT's MCP server lives at `https://ifttt.com/mcp`\n\nand uses **Streamable HTTP** transport. It expects authenticated HTTP POST requests and responds with either JSON or Server-Sent Events streams.\n\nTwo completely different transport layers. They don't talk to each other.\n\nSo what do you do? You build a proxy.\n\nWell... \"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.\n\nI guided the architecture, debugged alongside it, and steered the fixes when things broke. But the actual code? That was all Quick guiding [Kiro](https://kiro.dev/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253). This whole post is really about what happens when you pair an AI coding assistant with a well-defined integration problem.\n\nThe proxy is a ~500-line Node.js script that sits between them:\n\n```\n┌────────────┐  stdio    ┌───────────┐  HTTPS  ┌──────────┐\n│            │ JSON-RPC  │           │  POST   │          │\n│   Amazon   │ ────────▶ │   MCP     │ ──────▶ │  IFTTT   │\n│   Quick    │           │   Proxy   │         │  MCP     │\n│            │ ◀──────── │  (Node)   │ ◀────── │ (Remote) │\n│            │ JSON-RPC  │           │ SSE/JSON│          │\n└────────────┘           └─────┬─────┘         └──────────┘\n     local                     │                  remote\n                        ┌──────┴──────┐\n                        │ OAuth 2.1   │\n                        │ PKCE + Auto │\n                        │ Refresh     │\n                        └─────────────┘\n```\n\nIt 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.\n\nThe full flow:\n\nSounds straightforward? It mostly is. But two gotchas took me while to debug. Let me walk you through them.\n\nFirst things first. IFTTT requires OAuth authentication. The proxy has an `--auth`\n\nmode that handles the entire flow:\n\n``` js\nasync function authenticate() {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = generateCodeChallenge(codeVerifier);\n  const state = generateState();\n\n  const authParams = new URLSearchParams({\n    client_id: CLIENT_ID,\n    code_challenge: codeChallenge,\n    code_challenge_method: 'S256',\n    redirect_uri: REDIRECT_URI,\n    resource: 'https://ifttt.com/mcp',\n    response_type: 'code',\n    scope: 'mcp',\n    state: state,\n  });\n\n  // Opens browser, starts local callback server on port 3118\n  // Exchanges code for token using PKCE verifier\n  // Saves token to ~/.quickwork/ifttt-token.json\n}\n```\n\nRun `node index.js --auth`\n\nonce, authenticate in your browser, and the token gets saved locally. After that, the proxy handles refresh automatically. You never think about auth again.\n\nThe token management is simple but important:\n\n```\nfunction isTokenExpired(tokenData) {\n  if (!tokenData || !tokenData.access_token) return true;\n  if (!tokenData.expires_in) return false;\n  const expiresAt = tokenData.obtained_at + (tokenData.expires_in * 1000);\n  return Date.now() > expiresAt - 60000; // 1 minute buffer\n}\n```\n\nThat 60-second buffer matters. You don't want a request to fail because the token expires mid-flight.\n\nSo here's where it got interesting.\n\nMy first version of the proxy was dead simple. Read from stdin, POST to IFTTT, buffer the response, write to stdout. Classic request/response.\n\nIt worked great for `tools/list`\n\n. IFTTT returned a nice 200 OK with a JSON body listing all available tools. I was feeling good.\n\nThen I called `my_applets`\n\n.\n\nNothing came back. No error. No response. Just... silence.\n\nAfter 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.\n\nThe fix is a streaming-aware HTTP client that checks the `Content-Type`\n\nheader:\n\n```\nfunction httpsStreamingRequest(url, options, body, timeoutMs = 60000) {\n  return new Promise((resolve, reject) => {\n    const req = https.request(reqOptions, (res) => {\n      const contentType = res.headers['content-type'] || '';\n      const isSSE = contentType.includes('text/event-stream');\n\n      if (isSSE) {\n        // Keep the connection open, collect SSE events\n        let sseBuffer = '';\n        res.setEncoding('utf8');\n        res.on('data', (chunk) => { sseBuffer += chunk; });\n\n        res.on('end', () => {\n          resolve({\n            status: res.statusCode,\n            isSSE: true,\n            events: parseSSEBody(sseBuffer),\n          });\n        });\n      } else {\n        // Standard buffered response\n        let data = '';\n        res.on('data', (chunk) => { data += chunk; });\n        res.on('end', () => {\n          resolve({ status: res.statusCode, isSSE: false, body: data });\n        });\n      }\n    });\n\n    req.setTimeout(timeoutMs, () => {\n      req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));\n    });\n\n    if (body) req.write(body);\n    req.end();\n  });\n}\n```\n\nThe SSE parser itself is straightforward. Events are separated by double newlines, data lines start with `data:`\n\n:\n\n``` js\nfunction parseSSEBody(body) {\n  const events = [];\n  const blocks = body.split('\\n\\n');\n\n  for (const block of blocks) {\n    let eventData = '';\n    for (const line of block.split('\\n')) {\n      if (line.startsWith('data: ')) {\n        eventData += line.substring(6);\n      } else if (line.startsWith('data:')) {\n        eventData += line.substring(5);\n      }\n    }\n    if (eventData) {\n      try { events.push(JSON.parse(eventData)); } catch (e) {}\n    }\n  }\n  return events;\n}\n```\n\nAfter this fix, `my_applets`\n\nworked beautifully. IFTTT returned 12 applets, all properly structured. I was back to feeling good.\n\nFor about 10 minutes.\n\nSo 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.\n\nI pulled the raw JSON-RPC response to see what IFTTT was actually sending:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"result\": {\n    \"content\": [],\n    \"isError\": false,\n    \"structuredContent\": {\n      \"applets\": [...]\n    }\n  }\n}\n```\n\nSee it? The `content`\n\narray is **empty**. The actual data is in `structuredContent`\n\n.\n\nAccording to the MCP spec, tool results go in the `content`\n\narray as `TextContent`\n\nor `ImageContent`\n\nobjects. That's what Amazon Quick reads. IFTTT decided to put their data in a custom `structuredContent`\n\nfield instead, leaving `content`\n\nas an empty array.\n\nThe fix is a response transformer that runs before writing to stdout:\n\n```\nfunction transformToolResponse(jsonRpcResponse) {\n  if (!jsonRpcResponse || !jsonRpcResponse.result) return jsonRpcResponse;\n\n  const result = jsonRpcResponse.result;\n\n  if (\n    result.structuredContent &&\n    (!result.content || result.content.length === 0)\n  ) {\n    result.content = [\n      {\n        type: 'text',\n        text: JSON.stringify(result.structuredContent, null, 2),\n      },\n    ];\n  }\n\n  return jsonRpcResponse;\n}\n```\n\n12 lines. That's all it took. But finding the problem? That was the hard part.\n\nWith both gotchas solved, the main proxy loop is clean:\n\n``` js\nasync function proxyMcpRequest(jsonRpcMessage) {\n  const token = await getValidToken();\n\n  const headers = {\n    'Content-Type': 'application/json',\n    'Authorization': `Bearer ${token}`,\n    'Accept': 'application/json, text/event-stream',\n  };\n\n  if (mcpSessionId) {\n    headers['Mcp-Session-Id'] = mcpSessionId;\n  }\n\n  let response = await httpsStreamingRequest(IFTTT_MCP_URL, {\n    method: 'POST', headers\n  }, JSON.stringify(jsonRpcMessage));\n\n  // Capture session ID for subsequent requests\n  if (response.sessionId) {\n    mcpSessionId = response.sessionId;\n  }\n\n  // Handle 401 - try token refresh\n  if (response.status === 401) {\n    cachedToken = await refreshToken(cachedToken);\n    headers['Authorization'] = `Bearer ${cachedToken.access_token}`;\n    response = await httpsStreamingRequest(IFTTT_MCP_URL, {\n      method: 'POST', headers\n    }, JSON.stringify(jsonRpcMessage));\n  }\n\n  return response;\n}\n```\n\nThe `Accept: application/json, text/event-stream`\n\nheader is important. It tells IFTTT \"I can handle both formats.\" Without it, you might not get the SSE stream at all.\n\nThe proxy registers itself in the MCP config as a simple stdio server:\n\n```\n{\n  \"mcpServers\": {\n    \"ifttt\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/ifttt-mcp-proxy/index.js\"]\n    }\n  }\n}\n```\n\nThat'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.\n\nWith this proxy running, I can do all of this from my AI assistant using natural language:\n\nNo browser. No IFTTT web UI. Just conversational access to my entire automation setup.\n\nA few takeaways if you're building something similar:\n\n**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.\n\nIf you're working with MCP on AWS, [Amazon Bedrock Agents](https://aws.amazon.com/bedrock/agents/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253) supports MCP servers natively for remote tool use... so you might not need a custom proxy if you're already in that ecosystem.\n\n**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`\n\nbefore closing the connection.\n\n**Not everyone implements the spec the same way.** IFTTT's use of `structuredContent`\n\ninstead of `content[]`\n\nis technically non-standard. Your proxy might need to normalize responses.\n\n**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.\n\n**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.\n\nWhen 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](https://blog.technodrone.cloud/2026/05/your-coding-assistant-is-not-you.html) between developer and AI coding assistant... it's a relationship worth understanding.\n\nTools like the [AWS Toolkit for AI Agents](https://aws.amazon.com/developer/generative-ai/tools/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253) are making this kind of AI-assisted building the norm rather than the exception.\n\nThe full proxy is about 500 lines of zero-dependency Node.js. No npm install needed. Just `node`\n\nand the built-in `http`\n\n, `https`\n\n, and `crypto`\n\nmodules.\n\nThe [complete source code is on GitHub](https://github.com/maishsk/ifttt-mcp-proxy).\n\nI 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](https://twitter.com/maishsk) or [LinkedIn](https://www.linkedin.com/in/maishsk/) or feel free to leave a comment below.\n\nAnd if you're trying to connect other remote MCP servers to a local client...\n\nyour mileage may vary, but the pattern should be the same.", "url": "https://wpnews.pro/news/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy", "canonical_source": "https://dev.to/aws/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy-ind", "published_at": "2026-06-18 13:28:22+00:00", "updated_at": "2026-06-18 13:51:32.035207+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": ["IFTTT", "Amazon Quick", "Claude", "ChatGPT", "Kiro", "Node.js", "MCP"], "alternates": {"html": "https://wpnews.pro/news/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy", "markdown": "https://wpnews.pro/news/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy.md", "text": "https://wpnews.pro/news/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy.txt", "jsonld": "https://wpnews.pro/news/bridging-ifttt-to-your-local-ai-assistant-with-an-mcp-proxy.jsonld"}}