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 conversational intake forms that write directly to Notion.

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.

Two flows:

  • Creator — connect Notion, pick a DB, chat with Claude to configure the form (field contexts, slug). Publish. Share link.
  • Respondent — visit /f/slug, have a conversation, data lands in Notion.

Stack: TanStack Start · Anthropic Claude · Notion MCP (self-hosted on Railway) · Appwrite

Video Demo

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

Repo: https://github.com/immanewel/formlink

Live: https://formlink.appwrite.network

How I Used Notion MCP

Claude connects to a self-hosted Notion MCP server at runtime via Anthropic's mcp_servers API parameter:

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  system: buildSystemPrompt(form, schema),
  messages: history,
  mcp_servers: [{
    type: 'url',
    url: process.env.NOTION_MCP_URL,
    name: 'notion',
    authorization_token: notionToken
  }],
  tools: [{ type: 'mcp_toolset', mcp_server_name: 'notion' }]
}, { headers: { 'anthropic-beta': 'mcp-client-2025-11-20' } })
Enter fullscreen mode Exit fullscreen mode

Claude discovers notion_create_page 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.

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

Three things that tripped me up:

  1. Notion OAuth returns a bot user — the human identity is in tokenResponse.owner.user
  2. MCP response blocks are mcp_tool_use / mcp_tool_result, not the regular tool_use types
  3. System prompt must explicitly pass parent: { database_id } or Notion returns 400

One Cost Insight Worth Sharing

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

Top comments (0)