<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emmanuel Ng'wandu</title>
    <description>The latest articles on DEV Community by Emmanuel Ng'wandu (@emmanuel_ngwandu).</description>
    <link>https://dev.to/emmanuel_ngwandu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3838754%2F28225038-74cc-49f2-af1e-cf451423ddaa.jpg</url>
      <title>DEV Community: Emmanuel Ng'wandu</title>
      <link>https://dev.to/emmanuel_ngwandu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emmanuel_ngwandu"/>
    <language>en</language>
    <item>
      <title>I Built AI-Powered Forms That Write Directly to Notion — Using MCP at Runtime</title>
      <dc:creator>Emmanuel Ng'wandu</dc:creator>
      <pubDate>Sun, 29 Mar 2026 16:10:02 +0000</pubDate>
      <link>https://dev.to/emmanuel_ngwandu/i-built-ai-powered-forms-that-write-directly-to-notion-using-mcp-at-runtime-1kod</link>
      <guid>https://dev.to/emmanuel_ngwandu/i-built-ai-powered-forms-that-write-directly-to-notion-using-mcp-at-runtime-1kod</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Formlink&lt;/strong&gt; — AI-powered conversational intake forms that write directly to Notion.&lt;/p&gt;

&lt;p&gt;No static fields. Respondents have a back-and-forth chat with Claude. When enough info is collected, Claude calls Notion MCP and creates a structured row. No manual API wrapper. Claude decides when it has enough data.&lt;/p&gt;

&lt;p&gt;Two flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Creator&lt;/strong&gt; — connect Notion, pick a DB, chat with Claude to configure the form (field contexts, slug). Publish. Share link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respondent&lt;/strong&gt; — visit &lt;code&gt;/f/slug&lt;/code&gt;, have a conversation, data lands in Notion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; TanStack Start · Anthropic Claude · Notion MCP (self-hosted on Railway) · Appwrite&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/UZ6Iub6Y2jg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Creator sets up a form&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connect Notion → dashboard shows shared DBs → "Create Form" → two-panel screen. Claude asks ~5 questions: purpose, field contexts. Emits form config. Pick slug, hit Publish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Respondent fills it out&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Visit &lt;code&gt;formlink.appwrite.network/f/enterprise-software-dev-intake&lt;/code&gt; — no fields, no dropdowns. AI asks one question at a time, follows up on vague answers, skips irrelevant fields. When done, calls Notion MCP. Row created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data lands in Notion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open Notion — row is there. Every property filled. No sync, no Zapier, no webhook. Submission logged in dashboard with direct link to Notion page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us the code
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/immanewel/formlink" rel="noopener noreferrer"&gt;https://github.com/immanewel/formlink&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://formlink.appwrite.network" rel="noopener noreferrer"&gt;https://formlink.appwrite.network&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;Claude connects to a self-hosted Notion MCP server at runtime via Anthropic's &lt;code&gt;mcp_servers&lt;/code&gt; API parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-20250514&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_MCP_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorization_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;notionToken&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp_toolset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mcp_server_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic-beta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-client-2025-11-20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude discovers &lt;code&gt;notion_create_page&lt;/code&gt; dynamically — no manual tool schema. The system prompt tells it which database and what fields to collect. Claude handles the rest, including deciding when to write.&lt;/p&gt;

&lt;p&gt;What this unlocks: zero Notion API glue code. The AI decides when the form is complete and writes it. The app just checks for &lt;code&gt;mcp_tool_result&lt;/code&gt; in the response.&lt;/p&gt;

&lt;p&gt;Three things that tripped me up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Notion OAuth returns a bot user — the human identity is in &lt;code&gt;tokenResponse.owner.user&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MCP response blocks are &lt;code&gt;mcp_tool_use&lt;/code&gt; / &lt;code&gt;mcp_tool_result&lt;/code&gt;, not the regular &lt;code&gt;tool_use&lt;/code&gt; types&lt;/li&gt;
&lt;li&gt;System prompt must explicitly pass &lt;code&gt;parent: { database_id }&lt;/code&gt; or Notion returns 400&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  One Cost Insight Worth Sharing
&lt;/h3&gt;

&lt;p&gt;Exposing all Notion MCP tools loads every unused tool schema on every API call. Restricting to allowed_tools: ['notion_create_page'] eliminates that&lt;br&gt;
  overhead entirely — Formlink only ever creates pages, so there's no reason to pay for the rest.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
