This content originally appeared on DEV Community and was authored by cuongnp
Introduction
Hey developers! đź‘‹
MCP (Model Context Protocol) is everywhere these days, but let's be honest - most explanations are too abstract. Today, we'll get our hands dirty and build something real: a smart assistant that answers questions about your Notion tasks using OpenAI.
No fluff, just code. By the end of this post, you'll have a working MCP system that you can actually use!
What We'll Learn
- Build a complete MCP architecture from scratch
- Connect OpenAI (Host) ↔ Node.js (Client) ↔ Notion (Server)
- Implement JSON-RPC 2.0 communication
- Create a simple web UI for testing
- 80% hands-on coding, 20% theory ⚡
Prerequisites
Before we start coding:
- Notion account with a task database (enable developer mode)
- OpenAI API key
- Node.js installed
- Basic JavaScript knowledge
Tech Stack
- Node.js/Express - Backend server and MCP components
- OpenAI GPT - AI Host for natural language processing
- Notion API - Data source for tasks
- JSON-RPC 2.0 - Communication protocol between components
- HTML/JavaScript - Simple frontend UI
How It Works
Here's the architecture we're building:
The flow:
- User asks: "What tasks are due this week?"
- OpenAI (Host) calls MCP Client with context
- Client sends JSON-RPC request to MCP Server
- Server queries Notion API
- Data flows back up the chain
- OpenAI generates a natural language answer
Project Structure
mcp-demo/
├── server.js # Express app + Host entrypoint
├── mcpHost.js # OpenAI integration + orchestration
├── mcpClient.js # JSON-RPC client communication
├── mcpRpcServer.js # JSON-RPC server + Notion queries
├── notion.js # Notion API wrapper
├── gpt.js # OpenAI API wrapper
├── public/
│ └── index.html # Web UI
└── package.json
Step-by-Step Implementation
1. Set Up the Project
mkdir mcp-demo && cd mcp-demo
npm init -y
npm install express axios dotenv
Create .env
file:
NOTION_TOKEN=your_notion_integration_token
NOTION_DATABASE_ID=your_notion_database_id
OPENAI_API_KEY=your_openai_api_key
2. Build the Notion API Handler
Create notion.js
:
const axios = require('axios');
class NotionClient {
constructor() {
this.token = process.env.NOTION_TOKEN;
this.databaseId = process.env.NOTION_DATABASE_ID;
this.baseURL = 'https://api.notion.com/v1';
}
async queryTasks(filter = {}) {
try {
const response = await axios.post(
`${this.baseURL}/databases/${this.databaseId}/query`,
{ filter },
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
}
}
);
return this.formatTasks(response.data.results);
} catch (error) {
throw new Error(`Notion API error: ${error.message}`);
}
}
formatTasks(rawTasks) {
return rawTasks.map(task => ({
id: task.id,
title: task.properties.Name?.title[0]?.text?.content || 'Untitled',
status: task.properties.Status?.select?.name || 'No Status',
due_date: task.properties.Due?.date?.start || null,
priority: task.properties.Priority?.select?.name || 'Normal'
}));
}
}
module.exports = NotionClient;
3. Create the MCP Server (JSON-RPC)
Create mcpRpcServer.js
:
const express = require('express');
const NotionClient = require('./notion');
const app = express();
app.use(express.json());
const notionClient = new NotionClient();
// JSON-RPC 2.0 handler
app.post('/rpc', async (req, res) => {
const { jsonrpc, method, params, id } = req.body;
try {
let result;
switch (method) {
case 'getTasks':
result = await notionClient.queryTasks(params?.filter);
break;
case 'getTasksByStatus':
result = await notionClient.queryTasks({
property: 'Status',
select: { equals: params.status }
});
break;
default:
throw new Error(`Method not found: ${method}`);
}
res.json({ jsonrpc: '2.0', result, id });
} catch (error) {
res.json({
jsonrpc: '2.0',
error: { code: -32603, message: error.message },
id
});
}
});
const PORT = process.env.RPC_PORT || 3001;
app.listen(PORT, () => {
console.log(`🚀 MCP RPC Server running on port ${PORT}`);
});
4. Build the MCP Client
Create mcpClient.js
:
const axios = require('axios');
class MCPClient {
constructor(serverUrl = 'http://localhost:3001/rpc') {
this.serverUrl = serverUrl;
this.requestId = 0;
}
async call(method, params = {}) {
const payload = {
jsonrpc: '2.0',
method,
params,
id: ++this.requestId
};
try {
const response = await axios.post(this.serverUrl, payload, {
headers: { 'Content-Type': 'application/json' }
});
if (response.data.error) {
throw new Error(response.data.error.message);
}
return response.data.result;
} catch (error) {
throw new Error(`MCP Client error: ${error.message}`);
}
}
// Convenience methods
async getTasks() {
return this.call('getTasks');
}
async getTasksByStatus(status) {
return this.call('getTasksByStatus', { status });
}
}
module.exports = MCPClient;
5. Create the OpenAI Handler
Create gpt.js
:
const axios = require('axios');
class GPTClient {
constructor() {
this.apiKey = process.env.OPENAI_API_KEY;
this.baseURL = 'https://api.openai.com/v1';
}
async generateAnswer(userQuestion, contextData) {
const messages = [
{
role: 'system',
content: `You are a helpful assistant that answers questions about Notion tasks.
Use the provided context data to give accurate, friendly answers.
Context: ${JSON.stringify(contextData, null, 2)}`
},
{
role: 'user',
content: userQuestion
}
];
try {
const response = await axios.post(
`${this.baseURL}/chat/completions`,
{
model: 'gpt-3.5-turbo',
messages,
temperature: 0.7,
max_tokens: 500
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
);
return response.data.choices[0].message.content;
} catch (error) {
throw new Error(`OpenAI error: ${error.message}`);
}
}
}
module.exports = GPTClient;
6. Build the MCP Host
Create mcpHost.js
:
const MCPClient = require('./mcpClient');
const GPTClient = require('./gpt');
class MCPHost {
constructor() {
this.mcpClient = new MCPClient();
this.gptClient = new GPTClient();
}
async processQuestion(userQuestion) {
try {
// Determine what data we need based on the question
let contextData;
if (userQuestion.toLowerCase().includes('incomplete') ||
userQuestion.toLowerCase().includes('todo')) {
contextData = await this.mcpClient.getTasksByStatus('To Do');
} else if (userQuestion.toLowerCase().includes('done') ||
userQuestion.toLowerCase().includes('completed')) {
contextData = await this.mcpClient.getTasksByStatus('Done');
} else {
// Default: get all tasks
contextData = await this.mcpClient.getTasks();
}
// Generate natural language response
const answer = await this.gptClient.generateAnswer(userQuestion, contextData);
return {
success: true,
answer,
tasksCount: contextData.length
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
module.exports = MCPHost;
7. Create the Main Server
Create server.js
:
require('dotenv').config();
const express = require('express');
const path = require('path');
const MCPHost = require('./mcpHost');
const app = express();
app.use(express.json());
app.use(express.static('public'));
const mcpHost = new MCPHost();
// API endpoint for questions
app.post('/ask', async (req, res) => {
const { question } = req.body;
if (!question) {
return res.status(400).json({ error: 'Question is required' });
}
const result = await mcpHost.processQuestion(question);
res.json(result);
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🎉 MCP Demo running at http://localhost:${PORT}`);
});
8. Create the Web UI
Create public/index.html
:
<!DOCTYPE html>
<html>
<head>
<title>MCP Demo - Ask About Your Notion Tasks</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.chat-container { border: 1px solid #ddd; height: 400px; overflow-y: scroll; padding: 10px; margin: 10px 0; }
.message { margin: 10px 0; padding: 10px; border-radius: 5px; }
.user { background: #e3f2fd; text-align: right; }
.assistant { background: #f1f8e9; }
input[type="text"] { width: 70%; padding: 10px; }
button { padding: 10px 20px; background: #2196F3; color: white; border: none; border-radius: 5px; cursor: pointer; }
</style>
</head>
<body>
<h1>🤖 MCP Demo: Ask About Your Notion Tasks</h1>
<p>Try asking: "What tasks do I have?", "Show me incomplete tasks", "List done items"</p>
<div id="chat" class="chat-container"></div>
<input type="text" id="question" placeholder="Ask about your Notion tasks..." />
<button onclick="askQuestion()">Ask</button>
<script>
async function askQuestion() {
const question = document.getElementById('question').value;
if (!question.trim()) return;
addMessage(question, 'user');
document.getElementById('question').value = '';
try {
const response = await fetch('/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const result = await response.json();
if (result.success) {
addMessage(result.answer, 'assistant');
} else {
addMessage(`Error: ${result.error}`, 'assistant');
}
} catch (error) {
addMessage(`Network error: ${error.message}`, 'assistant');
}
}
function addMessage(text, sender) {
const chat = document.getElementById('chat');
const message = document.createElement('div');
message.className = `message ${sender}`;
message.textContent = text;
chat.appendChild(message);
chat.scrollTop = chat.scrollHeight;
}
document.getElementById('question').addEventListener('keypress', function(e) {
if (e.key === 'Enter') askQuestion();
});
</script>
</body>
</html>
🚀 Running Your MCP Demo
- Start the MCP RPC Server:
node mcpRpcServer.js
- Start the main application:
node server.js
Open your browser: Go to
http://localhost:3000
-
Ask questions like:
- "What tasks do I have?"
- "Show me incomplete items"
- "List all done tasks"
Database Notion:
Why MCP vs Function Calling?
Function Calling is OpenAI calling your functions directly. MCP is a standardized protocol where:
- Multiple AI systems can use the same servers
- Better separation of concerns
- Standardized communication via JSON-RPC
- More scalable for complex architectures
MCP vs HTTP APIs
Aspect | MCP | HTTP API |
---|---|---|
Protocol | JSON-RPC 2.0 | REST/GraphQL |
Standardization | High | Varies |
AI Integration | Native | Custom |
Tooling | Growing | Mature |
Next Steps
Want to level up? Try adding:
- Authentication for secure access
- Caching for better performance
- Multiple data sources (Google Calendar, Trello, etc.)
- Advanced filtering based on user intent
- Real-time updates with WebSockets
Reference
Conclusion
You just built a complete MCP system! 🎉 This architecture pattern is powerful because it standardizes how AI systems communicate with data sources. As MCP adoption grows, you'll be ahead of the curve.
What's next? Try this pattern with your own data sources. The possibilities are endless!
Want more hands-on AI tutorials? Check out my blog at techcodx.com for the latest in AI development, DevOps automation, and practical coding tutorials. Let's build the future together! 🚀
Found this helpful? Drop a ❤️ and share with your dev friends!
This content originally appeared on DEV Community and was authored by cuongnp

cuongnp | Sciencx (2025-10-13T13:51:11+00:00) Build Your First MCP Server with Notion and OpenAI – A Developer’s Guide. Retrieved from https://www.scien.cx/2025/10/13/build-your-first-mcp-server-with-notion-and-openai-a-developers-guide/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.