DEV Community

Cover image for I Built AI-Powered Forms That Write Directly to Notion — Using MCP at Runtime
Emmanuel Ng'wandu
Emmanuel Ng'wandu

Posted on

I Built AI-Powered Forms That Write Directly to Notion — Using MCP at Runtime

Notion MCP Challenge Submission 🧠

This is a submission for the Notion MCP Challenge


What I Built

Formlink — AI-powered intake system. Replaces static forms with natural conversations, writes structured data directly into Notion via MCP.

Instead of a 20-field form 60% of people abandon, Formlink has a 3-minute conversation — asking smart follow-ups, skipping irrelevant fields, writing clean structured data into Notion the moment the conversation ends.

Forms should listen, not interrogate.

What makes it different

1. AI branching — no conditional rules
A founder saying "just me, early stage" gets 3 questions. An enterprise team gets 6. Same form. Different conversations. No IF/THEN rules.

2. Creator field context — the real differentiator
Creators add intent per field:

"Budget Range — context: below $5K is self-serve tier, above gets personal follow-up"

AI reads this, asks smarter questions. Instead of "What's your budget?" it asks "What budget have you set aside — and is there flexibility if scope grows?" Nobody else does this.

3. MCP at runtime — not just during development
Claude connects to Notion MCP server during every form submission — discovering tools, calling notion_create_page, writing structured data to the creator's workspace. Not a REST API call dressed as MCP. Actual MCP tool execution at runtime.


Video Demo

No video — here's a written walkthrough instead.

1. Creator sets up a form
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.

2. Respondent fills it out
Visit formlink.appwrite.network/f/enterprise-software-dev-intake — 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.

3. Data lands in Notion
Open Notion — row is there. Every property filled. No sync, no Zapier, no webhook.
Submission logged in dashboard with direct link to Notion page.


Show Us the Code

Live demo: formlink.appwrite.network

GitHub: github.com/immanewel/formlink


How I Used Notion MCP

MCP is central to Formlink in two ways — at runtime and during development.

At Runtime — The Anthropic MCP Connector

Every form submission:

Respondent types answer
    ↓
TanStack Start server function
    ↓
Anthropic Messages API (with mcp_servers parameter)
    ↓
Claude connects to Notion MCP server on Railway
    ↓
Claude calls notion_create_page via MCP
    ↓
Structured row appears in creator's Notion database
Enter fullscreen mode Exit fullscreen mode
const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  system: buildSystemPrompt(form, schema),
  messages: conversationHistory,
  mcp_servers: [
    {
      type: 'url',
      url: process.env.NOTION_MCP_URL,
      name: 'notion',
      authorization_token: userNotionToken
    }
  ],
  tools: [
    {
      type: 'mcp_toolset',
      mcp_server_name: 'notion',
      allowed_tools: ['notion_create_page']   // 20x cheaper than exposing all tools
    }
  ]
}, {
  headers: { 'anthropic-beta': 'mcp-client-2025-11-20' }
})

const mcpResult = response.content.find(b => b.type === 'mcp_tool_result')
if (mcpResult) {
  await saveSubmission(form.$id, mcpResult)
  return { type: 'complete' }
}
Enter fullscreen mode Exit fullscreen mode

During Development — Claude Code + Notion MCP

Claude Code uses Notion MCP via stdio throughout the build. Before writing any integration code, used MCP to explore schema directly:

"Show me the properties of my Formlink — Consulting Intake database"
"Create a test page with these field values"
"What select options does Budget Range accept?"
Enter fullscreen mode Exit fullscreen mode

Every piece of code already worked before writing it — property names, types, values all verified via MCP first.

Same MCP server, two modes:

  • Dev: claude code → Notion MCP via stdio → explore, test, verify
  • Runtime: Anthropic API → Notion MCP via HTTP → create pages during submissions

Architecture

┌─────────────────────────────────────────────────┐
│                  Formlink                        │
│                                                  │
│  Creator Dashboard (Appwrite auth)               │
│  → Connects Notion via OAuth                     │
│  → Picks database → Conversational form setup    │
│  → Gets shareable link: /f/[slug]                │
│                                                  │
│  Public Intake Route (/f/[slug])                 │
│  → Respondent chats (no account needed)          │
│  → Server function calls Claude API              │
│  → Claude connects to Notion MCP server          │
│  → notion_create_page called via MCP             │
│  → Row lands in creator's Notion database        │
└─────────────────────────────────────────────────┘

Infrastructure:
  App:        Appwrite Sites (TanStack Start, SSR)
  Auth + DB:  Appwrite (auth, forms, submissions)
  Notion MCP: @notionhq/notion-mcp-server on Railway (HTTP/SSE)
  AI:         Anthropic Claude Sonnet via MCP connector
Enter fullscreen mode Exit fullscreen mode

One Cost Insight Worth Sharing

Exposing all Notion MCP tools loads every tool schema on every API call (~8,000 tokens overhead). Restricting to allowed_tools: ['notion_create_page'] drops this to ~400 tokens — 20x cost reduction. Cost per submission goes from ~$1.00 to ~$0.05.


The Stack

Layer Choice
Framework TanStack Start
Auth + DB Appwrite
AI Anthropic Claude Sonnet
Notion Notion MCP (runtime + dev)
MCP Host Railway
Hosting Appwrite Sites

What's Next

Phase 1 — done:

  • Notion OAuth per user — any creator connects their own workspace
  • Dynamic schema — any Notion database, not just the demo
  • Form expiration + custom slugs
  • Submission tracking dashboard

Top comments (0)