Meeting notes contain action items that often need context: competitive research, pricing lookups, feature comparisons, or market data. An MCP pipeline can automatically extract action items from meeting transcripts and enrich each one with relevant search data, turning vague tasks into actionable briefs. This tutorial builds an MCP tool server with two tools: one that parses meeting notes into structured actions, and one that enriches each action with web search context. Search enrichment costs $0.005 per action item via Scavio.
Prerequisites
- Python 3.10+ installed
- mcp and openai packages installed
- A Scavio API key from scavio.dev
- An OpenAI API key for action extraction
Walkthrough
Step 1: Build the action extraction tool
Create an MCP tool that takes raw meeting notes and uses an LLM to extract structured action items with assignees, deadlines, and search queries.
from mcp.server import Server
import json
from openai import OpenAI
server = Server('meeting-actions')
llm = OpenAI()
@server.tool()
async def extract_actions(meeting_notes: str) -> str:
"""Extract action items from meeting notes."""
response = llm.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'system', 'content': '''Extract action items from meeting notes.
Return JSON array with: task, assignee, deadline, search_query (a web search
that would provide useful context for this task).'''},
{'role': 'user', 'content': meeting_notes}],
response_format={'type': 'json_object'},
max_tokens=1000
)
return response.choices[0].message.contentStep 2: Build the search enrichment tool
Create a second MCP tool that takes an action item and enriches it with relevant web search data. This turns a bare task into a contextual brief.
import requests, os
API_KEY = os.environ['SCAVIO_API_KEY']
@server.tool()
async def enrich_action(search_query: str, task_context: str = '') -> str:
"""Enrich an action item with web search context."""
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': API_KEY, 'Content-Type': 'application/json'},
json={'query': search_query, 'country_code': 'us'})
results = resp.json().get('organic_results', [])[:5]
enrichment = {
'query': search_query,
'key_findings': [
{'source': r['link'], 'title': r['title'],
'insight': r.get('snippet', '')}
for r in results
],
'sources_count': len(results)
}
return json.dumps(enrichment, indent=2)Step 3: Build the full pipeline tool
Create a combined tool that does both extraction and enrichment in one call. This is the main entry point for the meeting-to-actions pipeline.
@server.tool()
async def meeting_to_actions(meeting_notes: str) -> str:
"""Extract action items from meeting notes and enrich each with search context."""
# Step 1: Extract actions
actions_json = await extract_actions(meeting_notes)
actions = json.loads(actions_json).get('actions', [])
# Step 2: Enrich each action
enriched = []
for action in actions:
search_query = action.get('search_query', action.get('task', ''))
enrichment_json = await enrich_action(search_query, action.get('task', ''))
enrichment = json.loads(enrichment_json)
enriched.append({
**action,
'enrichment': enrichment
})
result = {
'total_actions': len(enriched),
'credits_used': len(enriched), # 1 search per action
'cost': f'${len(enriched) * 0.005:.3f}',
'actions': enriched
}
return json.dumps(result, indent=2)Step 4: Run the MCP server
Start the server so it can be connected to Claude Desktop, Cursor, or any MCP client.
import asyncio
from mcp.server.stdio import stdio_server
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == '__main__':
asyncio.run(main())
# .mcp.json configuration:
# {
# "mcpServers": {
# "meeting-actions": {
# "command": "python",
# "args": ["meeting_actions_server.py"],
# "env": {
# "SCAVIO_API_KEY": "your_key",
# "OPENAI_API_KEY": "your_key"
# }
# }
# }
# }Step 5: Test with sample meeting notes
Feed in a real meeting transcript and verify the pipeline extracts and enriches actions correctly.
# Test the pipeline directly (without MCP client):
import asyncio
async def test():
notes = """Product sync 2026-05-13:
- Sarah: Research competitor pricing for CRM tools, need comparison by Friday
- Mike: Look into TikTok API options for our influencer vetting feature
- Lisa: Find best practices for AI Overview optimization, present next week
- Team: Evaluate n8n vs Zapier for our lead gen automation"""
result = await meeting_to_actions(notes)
data = json.loads(result)
print(f'Extracted {data["total_actions"]} actions (cost: {data["cost"]})')
for a in data['actions']:
print(f'\n Task: {a["task"]}')
print(f' Assignee: {a.get("assignee", "unassigned")}')
print(f' Context: {len(a["enrichment"]["key_findings"])} sources found')
asyncio.run(test())Python Example
import os, json, requests
from openai import OpenAI
llm = OpenAI()
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
def extract_actions(notes):
resp = llm.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'system', 'content': 'Extract action items as JSON array with task, assignee, search_query fields.'},
{'role': 'user', 'content': notes}],
response_format={'type': 'json_object'}, max_tokens=500)
return json.loads(resp.choices[0].message.content).get('actions', [])
def enrich(query):
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us'})
return [r.get('snippet', '') for r in resp.json().get('organic_results', [])[:3]]
notes = 'Sarah: Research CRM pricing. Mike: Evaluate TikTok APIs.'
for action in extract_actions(notes):
context = enrich(action.get('search_query', action['task']))
print(f'{action["task"]}: {len(context)} sources')JavaScript Example
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
async function enrich(query) {
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, country_code: 'us' })
});
const data = await resp.json();
return (data.organic_results || []).slice(0, 3)
.map(r => ({ title: r.title, snippet: r.snippet || '' }));
}
async function main() {
const actions = [
{ task: 'Research CRM pricing', query: 'CRM pricing comparison 2026' },
{ task: 'Evaluate TikTok APIs', query: 'TikTok API options developers 2026' }
];
for (const a of actions) {
const context = await enrich(a.query);
console.log(`${a.task}: ${context.length} sources found`);
}
}
main().catch(console.error);Expected Output
Extracted 4 actions (cost: $0.020)
Task: Research competitor CRM pricing comparison
Assignee: Sarah
Context: 5 sources found
Task: Evaluate TikTok API options for influencer vetting
Assignee: Mike
Context: 5 sources found
Task: Research AI Overview optimization best practices
Assignee: Lisa
Context: 5 sources found
Task: Compare n8n vs Zapier for lead generation
Assignee: Team
Context: 5 sources found