<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ API</title><link>https://blog.iany.me/tags/api/</link><description>Recent content in API «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Fri, 24 Jul 2026 19:48:12 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/api/index.xml" rel="self" type="application/rss+xml"/><item><title>Build an MCP with OAuth on Cloudflare Workers</title><link>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</link><pubDate>Fri, 24 Jul 2026 19:48:12 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</guid><description>&lt;p&gt;I wanted MCP clients to create Google Tasks for me. The Google Tasks API is gated behind a business license I don&amp;rsquo;t have. IFTTT already bridges webhooks to Google Tasks—but a Maker webhook URL is a bearer secret. Putting that key in local MCP config is a bad idea.&lt;/p&gt;
&lt;p&gt;The fix is a thin Cloudflare Worker plus &lt;strong&gt;Cloudflare Access Managed OAuth&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Worker holds the IFTTT key as a secret and speaks MCP over HTTP&lt;/li&gt;
&lt;li&gt;Access sits in front, handles OAuth login with your IdP&lt;/li&gt;
&lt;li&gt;Worker verifies the Access JWT (&lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;) before running tools&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full example: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;. Most of it was scaffolded by AI; the interesting part is the shape, not the TypeScript.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Tasks API&lt;/td&gt;
&lt;td&gt;Needs a Workspace-style setup I don&amp;rsquo;t want to pay for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFTTT Maker webhook&lt;/td&gt;
&lt;td&gt;Works, but the URL &lt;em&gt;is&lt;/em&gt; the credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP clients&lt;/td&gt;
&lt;td&gt;Expect remote HTTP tools with OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;MCP Client
→ Cloudflare Access (OAuth)
→ Worker (JWT)
→ IFTTT
→ Google Tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clients never see &lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;. They only hold a short-lived Access token after login.&lt;/p&gt;
&lt;h2 id="ifttt-side"&gt;IFTTT side&lt;/h2&gt;
&lt;p&gt;One applet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If&lt;/strong&gt;: Maker Webhooks → Receive a web request, event name &lt;code&gt;task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then&lt;/strong&gt;: Google Tasks → Create a task (title / notes / due from the payload)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Worker posts to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://maker.ifttt.com/trigger/task/json/with/key/{KEY}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with &lt;code&gt;{ &amp;quot;title&amp;quot;, &amp;quot;notes?&amp;quot;, &amp;quot;due?&amp;quot; }&lt;/code&gt;. The key never leaves Cloudflare.&lt;/p&gt;
&lt;h2 id="three-secrets"&gt;Three secrets&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-pwsh"&gt;npx wrangler secret put IFTTT_MAKER_TASK_KEY
npx wrangler secret put CF_ACCESS_TEAM_DOMAIN
npx wrangler secret put CF_ACCESS_AUD
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maker webhook key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_TEAM_DOMAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero Trust team domain (e.g. &lt;code&gt;myteam.cloudflareaccess.com&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_AUD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Access application audience tag (validates JWT &lt;code&gt;aud&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No custom authorize page, no signing keys you mint yourself. Access is the authorization server.&lt;/p&gt;
&lt;h2 id="cloudflare-access"&gt;Cloudflare Access&lt;/h2&gt;
&lt;p&gt;In the &lt;a href="https://one.dash.cloudflare.com/"&gt;Zero Trust dashboard&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Access&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;Add&lt;/strong&gt; → &lt;strong&gt;Self-hosted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Point the app at your Worker hostname (&lt;code&gt;*.workers.dev&lt;/code&gt; or a custom domain)&lt;/li&gt;
&lt;li&gt;Pick an IdP (Google, GitHub, email OTP, …) and an allow policy for yourself&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Managed OAuth (Beta)&lt;/strong&gt; — this is what MCP clients need for the OAuth dance&lt;/li&gt;
&lt;li&gt;Copy the team domain and Application Audience (AUD)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Access issues JWTs; MCP clients send them as &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;. The Worker verifies with the team&amp;rsquo;s JWKS:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;const JWKS = createRemoteJWKSet(
new URL(`${issuer}/cdn-cgi/access/certs`),
);
await jwtVerify(token, JWKS, {
issuer,
...(env.CF_ACCESS_AUD ? { audience: env.CF_ACCESS_AUD } : {}),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;jose&lt;/code&gt; on Workers with &lt;code&gt;nodejs_compat&lt;/code&gt;.)&lt;/p&gt;
&lt;h2 id="worker-just-mcp--verify"&gt;Worker: just MCP + verify&lt;/h2&gt;
&lt;p&gt;Business logic is tiny:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /&lt;/code&gt; or &lt;code&gt;POST /mcp&lt;/code&gt; — JSON-RPC (&lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;One tool: &lt;code&gt;create_google_task&lt;/code&gt; (&lt;code&gt;title&lt;/code&gt; required; &lt;code&gt;notes&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt; optional)&lt;/li&gt;
&lt;li&gt;Reject anything without a valid Access JWT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No OAuth endpoints in the Worker. Access owns login; the Worker only checks the assertion header.&lt;/p&gt;
&lt;h2 id="client-config"&gt;Client config&lt;/h2&gt;
&lt;p&gt;Point the client at the Worker URL and enable OAuth. Example shape (keys vary by client):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;mcp&amp;quot;: {
&amp;quot;google-tasks&amp;quot;: {
&amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
&amp;quot;url&amp;quot;: &amp;quot;https://google-task-mcp.yourdomain.com&amp;quot;
}
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client discovers OAuth via Access, opens the browser for your IdP, then attaches the Access JWT on MCP calls.&lt;/p&gt;
&lt;h2 id="why-this-shape-works-with-ai"&gt;Why this shape works with AI&lt;/h2&gt;
&lt;p&gt;The task decomposes cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MCP handler&lt;/strong&gt; — fixed JSON-RPC + one tool schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IFTTT &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt; — secret from &lt;code&gt;env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access JWT verify&lt;/strong&gt; — JWKS + &lt;code&gt;aud&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrangler secrets&lt;/strong&gt; — nothing in git&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Describe the constraints (&amp;ldquo;don&amp;rsquo;t expose the IFTTT URL&amp;rdquo;, &amp;ldquo;Cloudflare Access OAuth&amp;rdquo;, &amp;ldquo;Worker MCP&amp;rdquo;) and get a deployable skeleton. Review JWT verification and secret handling; treat the tool body as glue.&lt;/p&gt;
&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Skip the official Tasks API if IFTTT already has the integration.&lt;/li&gt;
&lt;li&gt;Keep Maker keys in Worker secrets, not client config.&lt;/li&gt;
&lt;li&gt;Cloudflare Access Managed OAuth turns IdP login into MCP-friendly OAuth without rolling your own authorize/token endpoints.&lt;/li&gt;
&lt;li&gt;The Worker only needs to verify &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt; and call IFTTT.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;github.com/doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/api/">API</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/security/">Security</category><category domain="https://blog.iany.me/tags/tool/">Tool</category></item></channel></rss>