Building from Source
Complete guide to building an IETF vCon-compliant MCP Server from scratch
📋 Table of Contents
Prerequisites
Required Knowledge
✅ TypeScript/JavaScript programming
✅ Node.js and npm/yarn
✅ PostgreSQL basics
✅ REST APIs and JSON
✅ Git version control
Required Software
✅ Node.js 18.x or higher (Download)
✅ npm or yarn package manager
✅ Git (Download)
✅ Code editor (VS Code recommended)
✅ Supabase account (Sign up)
Time Estimate
Total: 4-6 hours for first-time implementation
Experienced developers: 2-3 hours
Project Overview
What We're Building
An MCP (Model Context Protocol) Server that:
Stores and manages IETF vCon (Virtual Conversation) data
Provides tools for AI assistants to interact with vCons
Ensures full compliance with
draft-ietf-vcon-vcon-core-00
Uses Supabase (PostgreSQL) as the database backend
Key Features
✅ Create, read, update, delete vCons
✅ Add dialog, analysis, and attachments
✅ Search and query vCon data
✅ Privacy and consent management
✅ Spec-compliant data validation
✅ MCP tools for AI integration
Architecture
┌─────────────────┐
│ AI Assistant │ (Claude, etc.)
└────────┬────────┘
│ MCP Protocol
┌────────▼────────┐
│ MCP Server │ (This project)
└────────┬────────┘
│ Supabase Client
┌────────▼────────┐
│ Supabase │ (PostgreSQL + REST API)
└─────────────────┘
Phase 1: Environment Setup
Step 1.1: Create Project Directory
# Create project directory
mkdir vcon-mcp-server
cd vcon-mcp-server
# Initialize git repository
git init
Step 1.2: Initialize Node.js Project
# Create package.json
npm init -y
Step 1.3: Install Dependencies
# Core dependencies
npm install @modelcontextprotocol/sdk @supabase/supabase-js zod
# Development dependencies
npm install -D typescript @types/node tsx vitest eslint \
@typescript-eslint/eslint-plugin @typescript-eslint/parser
What each package does:
@modelcontextprotocol/sdk
- MCP server framework@supabase/supabase-js
- Supabase client libraryzod
- Schema validation librarytypescript
- TypeScript compilertsx
- TypeScript execution for developmentvitest
- Testing framework
Step 1.4: Configure TypeScript
Create tsconfig.json
:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
Step 1.5: Update package.json Scripts
Add these scripts to your package.json
:
{
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:compliance": "vitest run tests/vcon-compliance.test.ts",
"lint": "eslint src/**/*.ts"
}
}
Step 1.6: Create .env.example
Create .env.example
:
# Supabase Configuration
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key-here
# Optional: For service role operations
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Step 1.7: Create .gitignore
Create .gitignore
:
# Node
node_modules/
npm-debug.log*
package-lock.json
# TypeScript
dist/
*.tsbuildinfo
# Environment variables
.env
.env.local
# IDE
.vscode/
.idea/
.DS_Store
# Testing
coverage/
# Logs
*.log
✅ Phase 1 Checkpoint
Verify your setup:
# Check Node.js version
node --version # Should be 18.x or higher
# Check TypeScript
npx tsc --version
# Verify package.json
cat package.json
# Check dependencies
npm list
Phase 2: Database Setup
Step 2.1: Create Supabase Project
Go to supabase.com
Click "New Project"
Fill in:
Name: vcon-mcp-server
Database Password: (Generate strong password)
Region: (Choose closest to you)
Click "Create new project"
Wait 2-3 minutes for provisioning
Step 2.2: Get Supabase Credentials
Go to Project Settings → API
Copy:
Project URL → SUPABASE_URL
anon/public key → SUPABASE_ANON_KEY
Create .env
file:
SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Step 2.3: Run Database Schema
Go to SQL Editor in Supabase dashboard
Copy the entire schema from
CORRECTED_SCHEMA.md
Paste into SQL Editor
Click "Run" to execute
Key tables created:
vcons
- Main vCon recordsparties
- Conversation participantsdialog
- Conversation content (recordings, text, etc.)analysis
- AI/ML analysis resultsattachments
- File attachmentsparty_history
- Party event timeline
Step 2.4: Verify Database Schema
Run these verification queries in SQL Editor:
-- Check tables exist
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- Check analysis table has 'schema' field (not 'schema_version')
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'analysis'
ORDER BY ordinal_position;
-- Verify vendor is NOT NULL
SELECT column_name, is_nullable
FROM information_schema.columns
WHERE table_name = 'analysis'
AND column_name = 'vendor';
-- Should show 'NO' for is_nullable
-- Verify dialog type constraint exists
SELECT constraint_name, check_clause
FROM information_schema.check_constraints
WHERE constraint_name = 'dialog_type_check';
-- Check that encoding fields have no default values
SELECT column_name, column_default
FROM information_schema.columns
WHERE table_name IN ('dialog', 'analysis', 'attachments')
AND column_name = 'encoding';
-- All should show NULL for column_default
Expected results:
✅ 8+ tables created
✅
analysis.schema
exists (NOTschema_version
)✅
analysis.vendor
is NOT NULL✅
analysis.body
is TEXT type✅ No DEFAULT values on encoding fields
✅ Dialog type constraint exists
Step 2.5: Set Up Row Level Security (Optional)
For multi-tenant applications, enable RLS:
-- Enable RLS on all tables
ALTER TABLE vcons ENABLE ROW LEVEL SECURITY;
ALTER TABLE parties ENABLE ROW LEVEL SECURITY;
ALTER TABLE dialog ENABLE ROW LEVEL SECURITY;
ALTER TABLE analysis ENABLE ROW LEVEL SECURITY;
ALTER TABLE attachments ENABLE ROW LEVEL SECURITY;
-- Example policy: Users can only access their own vCons
CREATE POLICY "Users can view own vcons"
ON vcons FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create own vcons"
ON vcons FOR INSERT
WITH CHECK (auth.uid() = user_id);
✅ Phase 2 Checkpoint
Verify database setup:
# Test connection with a simple query
node -e "
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
const { data, error } = await supabase.from('vcons').select('count');
console.log('Connection test:', error ? 'FAILED' : 'SUCCESS');
"
Phase 3: Project Structure
Step 3.1: Create Directory Structure
# Create all directories
mkdir -p src/{types,tools,resources,prompts,db,utils}
mkdir -p tests
mkdir -p scripts
Step 3.2: Verify Structure
Your project should now look like this:
vcon-mcp-server/
├── src/
│ ├── types/ # TypeScript type definitions
│ ├── tools/ # MCP tool implementations
│ ├── resources/ # MCP resource handlers
│ ├── prompts/ # MCP prompt templates
│ ├── db/ # Database client and queries
│ └── utils/ # Utility functions
├── tests/ # Test files
├── scripts/ # Build and maintenance scripts
├── package.json
├── tsconfig.json
├── .env
├── .env.example
└── .gitignore
✅ Phase 3 Checkpoint
# Verify directory structure
ls -R src/
Phase 4: Core Implementation
Step 4.1: Create vCon Types
Create src/types/vcon.ts
:
/**
* IETF vCon Core Types - Compliant with draft-ietf-vcon-vcon-core-00
* CRITICAL: Uses corrected field names per specification
*/
export type VConVersion = '0.3.0';
export type Encoding = 'base64url' | 'json' | 'none';
export type DialogType = 'recording' | 'text' | 'transfer' | 'incomplete';
// Section 4.2 - Party Object
export interface Party {
tel?: string;
sip?: string;
stir?: string;
mailto?: string;
name?: string;
did?: string;
validation?: string;
jcard?: object;
gmlpos?: string;
civicaddress?: object;
timezone?: string;
uuid?: string; // Section 4.2.12 - REQUIRED FIELD
}
// Section 4.3 - Dialog Object
export interface Dialog {
type: DialogType;
start?: string;
duration?: number;
parties?: number | number[] | (number | number[])[];
originator?: number;
mediatype?: string;
filename?: string;
body?: string;
encoding?: Encoding;
url?: string;
content_hash?: string | string[];
disposition?: 'no-answer' | 'congestion' | 'failed' | 'busy' | 'hung-up' | 'voicemail-no-message';
session_id?: string; // Section 4.3.10
application?: string; // Section 4.3.13
message_id?: string; // Section 4.3.14
}
// Section 4.5 - Analysis Object
// ⚠️ CRITICAL: This is the CORRECTED version
export interface Analysis {
type: string;
dialog?: number | number[];
mediatype?: string;
filename?: string;
vendor: string; // ✅ REQUIRED per spec Section 4.5.5
product?: string;
schema?: string; // ✅ CORRECT: Not 'schema_version'
body?: string; // ✅ CORRECT: String, not object
encoding?: Encoding;
url?: string;
content_hash?: string | string[];
}
// Section 4.4 - Attachment Object
export interface Attachment {
type?: string;
start?: string;
party?: number;
dialog?: number;
mediatype?: string;
filename?: string;
body?: string;
encoding?: Encoding;
url?: string;
content_hash?: string | string[];
}
// Section 4.1 - Main vCon Object
export interface VCon {
vcon: VConVersion;
uuid: string;
extensions?: string[]; // Section 4.1.3
must_support?: string[]; // Section 4.1.4
created_at: string;
updated_at?: string;
subject?: string;
redacted?: {
uuid?: string;
type?: string;
url?: string;
content_hash?: string | string[];
};
appended?: {
uuid?: string;
url?: string;
content_hash?: string | string[];
};
group?: Array<{
uuid?: string;
body?: string;
encoding?: 'json';
url?: string;
content_hash?: string | string[];
}>;
parties: Party[];
dialog?: Dialog[];
analysis?: Analysis[];
attachments?: Attachment[];
}
// Validation helper functions
export function isValidDialogType(type: string): type is DialogType {
return ['recording', 'text', 'transfer', 'incomplete'].includes(type);
}
export function isValidEncoding(encoding: string): encoding is Encoding {
return ['base64url', 'json', 'none'].includes(encoding);
}
💡 Key Points:
✅ Uses
schema
notschema_version
in Analysis✅
vendor
is required (no?
) in Analysis✅
body
isstring
type (notobject
)✅ Includes
uuid
field in Party✅ Includes new fields:
session_id
,application
,message_id
Step 4.2: Create Database Client
Create src/db/client.ts
:
import { createClient, SupabaseClient } from '@supabase/supabase-js';
let supabase: SupabaseClient | null = null;
export function getSupabaseClient(): SupabaseClient {
if (!supabase) {
const url = process.env.SUPABASE_URL;
const key = process.env.SUPABASE_ANON_KEY;
if (!url || !key) {
throw new Error(
'Missing Supabase credentials. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.'
);
}
supabase = createClient(url, key);
}
return supabase;
}
export function closeSupabaseClient(): void {
supabase = null;
}
Step 4.3: Create Database Queries
Create src/db/queries.ts
:
import { SupabaseClient } from '@supabase/supabase-js';
import { VCon, Analysis, Dialog, Party } from '../types/vcon';
export class VConQueries {
constructor(private supabase: SupabaseClient) {}
async createVCon(vcon: VCon): Promise<{ uuid: string }> {
// Insert main vcon
const { data: vconData, error: vconError } = await this.supabase
.from('vcons')
.insert({
uuid: vcon.uuid,
vcon_version: vcon.vcon,
subject: vcon.subject,
created_at: vcon.created_at,
updated_at: vcon.updated_at,
extensions: vcon.extensions,
must_support: vcon.must_support,
})
.select('id, uuid')
.single();
if (vconError) throw vconError;
// Insert parties
if (vcon.parties.length > 0) {
const partiesData = vcon.parties.map((party, index) => ({
vcon_id: vconData.id,
party_index: index,
tel: party.tel,
sip: party.sip,
mailto: party.mailto,
name: party.name,
did: party.did,
uuid: party.uuid, // ✅ Corrected field
validation: party.validation,
}));
const { error: partiesError } = await this.supabase
.from('parties')
.insert(partiesData);
if (partiesError) throw partiesError;
}
return { uuid: vconData.uuid };
}
async addAnalysis(vconUuid: string, analysis: Analysis): Promise<void> {
const { data: vcon, error: vconError } = await this.supabase
.from('vcons')
.select('id')
.eq('uuid', vconUuid)
.single();
if (vconError) throw vconError;
// Get next analysis index
const { data: existingAnalysis } = await this.supabase
.from('analysis')
.select('analysis_index')
.eq('vcon_id', vcon.id)
.order('analysis_index', { ascending: false })
.limit(1);
const nextIndex = existingAnalysis?.length
? existingAnalysis[0].analysis_index + 1
: 0;
// ✅ CORRECTED: Use 'schema' not 'schema_version'
const { error: analysisError } = await this.supabase
.from('analysis')
.insert({
vcon_id: vcon.id,
analysis_index: nextIndex,
type: analysis.type,
dialog_indices: Array.isArray(analysis.dialog)
? analysis.dialog
: (analysis.dialog ? [analysis.dialog] : null),
mediatype: analysis.mediatype,
filename: analysis.filename,
vendor: analysis.vendor, // ✅ REQUIRED field
product: analysis.product,
schema: analysis.schema, // ✅ Correct field name
body: analysis.body, // ✅ TEXT type
encoding: analysis.encoding,
url: analysis.url,
content_hash: analysis.content_hash,
});
if (analysisError) throw analysisError;
}
async getVCon(uuid: string): Promise<VCon> {
// Get main vcon
const { data: vconData, error: vconError } = await this.supabase
.from('vcons')
.select('*')
.eq('uuid', uuid)
.single();
if (vconError) throw vconError;
// Get parties
const { data: parties } = await this.supabase
.from('parties')
.select('*')
.eq('vcon_id', vconData.id)
.order('party_index');
// Get dialog
const { data: dialogs } = await this.supabase
.from('dialog')
.select('*')
.eq('vcon_id', vconData.id)
.order('dialog_index');
// Get analysis - ✅ Queries 'schema' not 'schema_version'
const { data: analysis } = await this.supabase
.from('analysis')
.select('*')
.eq('vcon_id', vconData.id)
.order('analysis_index');
// Reconstruct vCon
return {
vcon: vconData.vcon_version as '0.3.0',
uuid: vconData.uuid,
extensions: vconData.extensions,
must_support: vconData.must_support,
created_at: vconData.created_at,
updated_at: vconData.updated_at,
subject: vconData.subject,
parties: parties?.map(p => ({
tel: p.tel,
sip: p.sip,
mailto: p.mailto,
name: p.name,
did: p.did,
uuid: p.uuid,
validation: p.validation,
})) || [],
dialog: dialogs?.map(d => ({
type: d.type,
start: d.start_time,
duration: d.duration_seconds,
parties: d.parties,
originator: d.originator,
mediatype: d.mediatype,
body: d.body,
encoding: d.encoding,
session_id: d.session_id,
application: d.application,
message_id: d.message_id,
})),
analysis: analysis?.map(a => ({
type: a.type,
dialog: a.dialog_indices,
vendor: a.vendor,
product: a.product,
schema: a.schema, // ✅ Correct field name
body: a.body,
encoding: a.encoding,
})),
};
}
async searchVCons(filters: {
subject?: string;
partyName?: string;
startDate?: string;
endDate?: string;
}): Promise<VCon[]> {
let query = this.supabase
.from('vcons')
.select('*');
if (filters.subject) {
query = query.ilike('subject', `%${filters.subject}%`);
}
if (filters.startDate) {
query = query.gte('created_at', filters.startDate);
}
if (filters.endDate) {
query = query.lte('created_at', filters.endDate);
}
const { data, error } = await query;
if (error) throw error;
// Fetch full vCons for results
return Promise.all(
data.map(v => this.getVCon(v.uuid))
);
}
}
Step 4.4: Create Validation Utilities
Create src/utils/validation.ts
:
import { VCon, Analysis, Dialog } from '../types/vcon';
export class VConValidator {
private errors: string[] = [];
validate(vcon: VCon): { valid: boolean; errors: string[] } {
this.errors = [];
this.validateVersion(vcon);
this.validateUUID(vcon.uuid);
this.validateParties(vcon.parties);
if (vcon.dialog) this.validateDialogs(vcon.dialog);
if (vcon.analysis) this.validateAnalysis(vcon.analysis);
return {
valid: this.errors.length === 0,
errors: this.errors
};
}
private validateVersion(vcon: VCon): void {
if (vcon.vcon !== '0.3.0') {
this.errors.push(`Invalid vcon version: ${vcon.vcon}`);
}
}
private validateUUID(uuid: string): void {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(uuid)) {
this.errors.push(`Invalid UUID format: ${uuid}`);
}
}
private validateParties(parties: any[]): void {
if (parties.length === 0) {
this.errors.push('vCon must have at least one party');
}
}
private validateDialogs(dialogs: Dialog[]): void {
dialogs.forEach((dialog, i) => {
const validTypes = ['recording', 'text', 'transfer', 'incomplete'];
if (!validTypes.includes(dialog.type)) {
this.errors.push(`Dialog ${i} has invalid type: ${dialog.type}`);
}
});
}
private validateAnalysis(analyses: Analysis[]): void {
analyses.forEach((analysis, i) => {
// ✅ CRITICAL: vendor is required
if (!analysis.vendor) {
this.errors.push(`Analysis ${i} missing required field: vendor`);
}
});
}
}
export function validateVCon(vcon: VCon) {
return new VConValidator().validate(vcon);
}
✅ Phase 4 Checkpoint
Verify your implementation:
# Compile TypeScript
npm run build
# Should compile without errors
# Check dist/ directory was created
ls dist/
Phase 5: MCP Server
Step 5.1: Create MCP Tool Definitions
Create src/tools/vcon-crud.ts
:
import { z } from 'zod';
// ✅ CORRECTED: Analysis schema with proper field names
export const AnalysisSchema = z.object({
type: z.string(),
dialog: z.union([z.number(), z.array(z.number())]).optional(),
vendor: z.string(), // ✅ REQUIRED
product: z.string().optional(),
schema: z.string().optional(), // ✅ Correct field name
body: z.string().optional(), // ✅ String type
encoding: z.enum(['base64url', 'json', 'none']).optional(),
});
export const PartySchema = z.object({
tel: z.string().optional(),
mailto: z.string().optional(),
name: z.string().optional(),
uuid: z.string().uuid().optional(), // ✅ Added
});
export const DialogSchema = z.object({
type: z.enum(['recording', 'text', 'transfer', 'incomplete']),
start: z.string().optional(),
body: z.string().optional(),
encoding: z.enum(['base64url', 'json', 'none']).optional(),
session_id: z.string().optional(),
application: z.string().optional(),
message_id: z.string().optional(),
});
// MCP Tool: Create vCon
export const createVConTool = {
name: 'create_vcon',
description: 'Create a new vCon compliant with IETF spec',
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
parties: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
tel: { type: 'string' },
mailto: { type: 'string' },
uuid: { type: 'string' },
}
},
minItems: 1
}
},
required: ['parties']
}
};
// MCP Tool: Add Analysis
export const addAnalysisTool = {
name: 'add_analysis',
description: 'Add analysis to a vCon',
inputSchema: {
type: 'object',
properties: {
vcon_uuid: { type: 'string', format: 'uuid' },
analysis: {
type: 'object',
properties: {
type: { type: 'string' },
vendor: { type: 'string' }, // ✅ REQUIRED
product: { type: 'string' },
schema: { type: 'string' }, // ✅ Correct name
body: { type: 'string' }, // ✅ String type
encoding: {
type: 'string',
enum: ['base64url', 'json', 'none']
}
},
required: ['type', 'vendor'] // ✅ vendor required
}
},
required: ['vcon_uuid', 'analysis']
}
};
// MCP Tool: Get vCon
export const getVConTool = {
name: 'get_vcon',
description: 'Retrieve a vCon by UUID',
inputSchema: {
type: 'object',
properties: {
uuid: { type: 'string', format: 'uuid' }
},
required: ['uuid']
}
};
// MCP Tool: Search vCons
export const searchVConsTool = {
name: 'search_vcons',
description: 'Search vCons by criteria',
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
party_name: { type: 'string' },
start_date: { type: 'string', format: 'date-time' },
end_date: { type: 'string', format: 'date-time' }
}
}
};
Step 5.2: Create MCP Server
Create src/index.ts
:
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { getSupabaseClient } from './db/client.js';
import { VConQueries } from './db/queries.js';
import { validateVCon } from './utils/validation.js';
import {
createVConTool,
addAnalysisTool,
getVConTool,
searchVConsTool,
} from './tools/vcon-crud.js';
import { VCon, Analysis } from './types/vcon.js';
// Initialize MCP server
const server = new Server(
{
name: 'vcon-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize database
const supabase = getSupabaseClient();
const queries = new VConQueries(supabase);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
createVConTool,
addAnalysisTool,
getVConTool,
searchVConsTool,
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'create_vcon': {
const vcon: VCon = {
vcon: '0.3.0',
uuid: crypto.randomUUID(),
created_at: new Date().toISOString(),
subject: args.subject,
parties: args.parties,
};
// Validate before saving
const validation = validateVCon(vcon);
if (!validation.valid) {
return {
content: [{
type: 'text',
text: `Validation failed: ${validation.errors.join(', ')}`,
}],
isError: true,
};
}
const result = await queries.createVCon(vcon);
return {
content: [{
type: 'text',
text: `Created vCon with UUID: ${result.uuid}`,
}],
};
}
case 'add_analysis': {
const analysis: Analysis = args.analysis;
// ✅ Ensure vendor is provided
if (!analysis.vendor) {
return {
content: [{
type: 'text',
text: 'Error: vendor is required in analysis',
}],
isError: true,
};
}
await queries.addAnalysis(args.vcon_uuid, analysis);
return {
content: [{
type: 'text',
text: `Added analysis to vCon ${args.vcon_uuid}`,
}],
};
}
case 'get_vcon': {
const vcon = await queries.getVCon(args.uuid);
return {
content: [{
type: 'text',
text: JSON.stringify(vcon, null, 2),
}],
};
}
case 'search_vcons': {
const results = await queries.searchVCons({
subject: args.subject,
partyName: args.party_name,
startDate: args.start_date,
endDate: args.end_date,
});
return {
content: [{
type: 'text',
text: JSON.stringify(results, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [{
type: 'text',
text: `Error: ${error.message}`,
}],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('vCon MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
Step 5.3: Test the Server
# Run in development mode
npm run dev
# In another terminal, test with MCP Inspector
npm run test:console
✅ Phase 5 Checkpoint
The server should:
✅ Start without errors
✅ List 4 tools (create, add_analysis, get, search)
✅ Accept tool calls
✅ Return proper responses
Phase 6: Testing & Validation
Step 6.1: Create Compliance Tests
Create tests/vcon-compliance.test.ts
:
import { describe, it, expect } from 'vitest';
import { VCon, Analysis } from '../src/types/vcon';
import { validateVCon } from '../src/utils/validation';
describe('IETF vCon Spec Compliance', () => {
it('should use "schema" not "schema_version"', () => {
const analysis: Analysis = {
type: 'test',
vendor: 'TestVendor',
schema: 'v1.0', // ✅ Correct
body: 'test',
encoding: 'none'
};
expect(analysis.schema).toBe('v1.0');
expect((analysis as any).schema_version).toBeUndefined();
});
it('should require vendor in analysis', () => {
const vcon: VCon = {
vcon: '0.3.0',
uuid: crypto.randomUUID(),
created_at: new Date().toISOString(),
parties: [{ name: 'Test' }],
analysis: [{
type: 'test',
body: 'test',
encoding: 'none'
} as any]
};
const result = validateVCon(vcon);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('vendor'))).toBe(true);
});
it('should accept string body in analysis', () => {
const analysis: Analysis = {
type: 'transcript',
vendor: 'TestVendor',
body: 'Plain text content', // ✅ String
encoding: 'none'
};
expect(typeof analysis.body).toBe('string');
});
it('should support party uuid field', () => {
const vcon: VCon = {
vcon: '0.3.0',
uuid: crypto.randomUUID(),
created_at: new Date().toISOString(),
parties: [{
name: 'Test',
uuid: crypto.randomUUID() // ✅ uuid field
}]
};
expect(vcon.parties[0].uuid).toBeDefined();
expect(validateVCon(vcon).valid).toBe(true);
});
});
Step 6.2: Run Tests
# Run all tests
npm test
# Run compliance tests only
npm run test:compliance
Step 6.3: Verify No Incorrect Field Names
# Search for incorrect field names in code
grep -r "schema_version" src/
# Should return NO results
grep -r "vendor?" src/types/
# Should return NO results (vendor should not be optional)
✅ Phase 6 Checkpoint
All tests should pass:
✅ No
schema_version
in codebase✅
vendor
is required in Analysis type✅
body
accepts string values✅ All validation tests pass
Phase 7: Deployment
Step 7.1: Build for Production
# Clean and build
rm -rf dist/
npm run build
# Verify build
ls dist/
Step 7.2: Configure MCP Client
Add to your MCP client configuration (e.g., Claude Desktop):
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"vcon": {
"command": "node",
"args": ["/absolute/path/to/vcon-mcp-server/dist/index.js"],
"env": {
"SUPABASE_URL": "https://xxxxx.supabase.co",
"SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiI..."
}
}
}
}
Step 7.3: Test with AI Assistant
Restart your AI assistant and try:
Create a new vCon with parties Alice and Bob
✅ Phase 7 Checkpoint
The AI should:
✅ See the vCon tools
✅ Successfully create vCons
✅ Add analysis with correct field names
✅ Retrieve and search vCons
Troubleshooting
Issue: TypeScript Compilation Errors
Symptom: Errors about missing types or incorrect field names
Solution:
# Reinstall dependencies
rm -rf node_modules package-lock.json
npm install
# Check TypeScript version
npx tsc --version # Should be 5.x
# Verify tsconfig.json is correct
cat tsconfig.json
Issue: Database Connection Fails
Symptom: "Missing Supabase credentials" error
Solution:
# Verify .env file exists and has correct values
cat .env
# Test connection
node -e "
require('dotenv').config();
console.log('URL:', process.env.SUPABASE_URL ? '✓' : '✗');
console.log('KEY:', process.env.SUPABASE_ANON_KEY ? '✓' : '✗');
"
Issue: "schema_version does not exist" Error
Symptom: Database error about unknown column
Solution:
-- Verify database schema is correct
SELECT column_name FROM information_schema.columns
WHERE table_name = 'analysis';
-- Should show 'schema' NOT 'schema_version'
If incorrect, re-run the corrected schema from CORRECTED_SCHEMA.md
.
Issue: Vendor Validation Fails
Symptom: "vendor is required" error
Solution:
✅ Always include
vendor
in analysis objects✅ Check that Analysis type doesn't have
vendor?
(with question mark)✅ Verify tool schema marks vendor as required
Issue: MCP Server Doesn't Start
Symptom: Server crashes on startup
Solution:
# Check for syntax errors
npm run build
# Run with detailed logging
NODE_ENV=development npm run dev
# Verify MCP SDK version
npm list @modelcontextprotocol/sdk
Next Steps
Enhancements to Consider
Add More Tools
Update vCon
Delete vCon
Add dialog
Add attachments
Export vCon to JWS/JWE
Add Resources
vcon://uuid
URI schemeList recent vCons
vCon templates
Add Prompts
Summarize conversation
Extract action items
Compliance check
Advanced Features
Privacy redaction
Consent management
Group vCons
Digital signatures (JWS)
Encryption (JWE)
Performance
Caching layer
Batch operations
Pagination
Indexes optimization
Security
Row Level Security
API rate limiting
Input sanitization
Audit logging
Learning Resources
IETF vCon Spec:
background_docs/draft-ietf-vcon-vcon-core-00.txt
MCP Documentation: https://modelcontextprotocol.io/
Supabase Docs: https://supabase.com/docs
Implementation Guide:
CLAUDE_CODE_INSTRUCTIONS.md
Quick Reference:
QUICK_REFERENCE.md
Community & Support
vCon Working Group: https://datatracker.ietf.org/wg/vcon/
MCP Discord: https://discord.gg/modelcontextprotocol
Issues: File bugs/questions in your repo issues
Appendix: Command Reference
Development Commands
# Install dependencies
npm install
# Build project
npm run build
# Run in development
npm run dev
# Run tests
npm test
npm run test:compliance
# Lint code
npm run lint
# Type check
npx tsc --noEmit
Git Commands
# Initialize repo
git init
# Add all files
git add -A
# Commit
git commit -m "Initial commit"
# Create remote and push
git remote add origin <url>
git push -u origin main
Database Commands
# Connect to Supabase
psql "postgresql://postgres:[email protected]:5432/postgres"
# List tables
\dt
# Describe table
\d analysis
# Run SQL file
\i schema.sql
Success Checklist
Your implementation is complete when:
🎉 Congratulations! You've built a fully spec-compliant IETF vCon MCP Server!
Last Updated: October 7, 2025 Spec Version: draft-ietf-vcon-vcon-core-00 vCon Schema: 0.3.0
Last updated