MCP

Intent Surfaces

A reference manual for people who design and build MCP (Model Context Protocol) ecosystems

MCP

Intent Surfaces

A reference manual for people who design and build MCP (Model Context Protocol) ecosystems

MCP

Intent Surfaces

A reference manual for people who design and build MCP (Model Context Protocol) ecosystems

The Tool Maker's Journey

The Tool Maker's Journey

The Tool Maker's Journey

From simple file readers to complex integrations • The art of exposing capabilities • When to use a hammer vs. a Swiss Army knife

From simple file readers to complex integrations • The art of exposing capabilities • When to use a hammer vs. a Swiss Army knife

From simple file readers to complex integrations • The art of exposing capabilities • When to use a hammer vs. a Swiss Army knife

The Philosophy of Good Tools

The Philosophy of Good Tools

In 1978, Brian Kernighan wrote: "Make each program do one thing well." This Unix philosophy has guided tool makers for decades. But with MCP, we face a new question: How do we make tools that AI can use well? The answer isn't in the code—it's in understanding how AI thinks about capabilities. Let's explore this through a real example. Here are two ways to build the same tool:

In 1978, Brian Kernighan wrote: "Make each program do one thing well." This Unix philosophy has guided tool makers for decades. But with MCP, we face a new question: How do we make tools that AI can use well? The answer isn't in the code—it's in understanding how AI thinks about capabilities. Let's explore this through a real example. Here are two ways to build the same tool:

❌ The "Everything Button"

❌ The "Everything Button"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  name: 'manage_files',
  description: 'Manage files - read, write, delete, move, copy, search, etc.',
  inputSchema: {
    type: 'object',
    properties: {
      operation: { 
        type: 'string',
        enum: ['read', 'write', 'delete', 'move', 'copy', 'search', 'list']
      },
      path: { type: 'string' },
      content: { type: 'string' },
      destination: { type: 'string' },
      query: { type: 'string' }
      // ... 15 more properties for different operations
    }
  }
}

✅ The Unix Philosophy Approach

✅ The Unix Philosophy Approach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[
  {
    name: 'read_file',
    description: 'Read contents of a file',
    inputSchema: {
      type: 'object',
      properties: {
        path: { type: 'string', description: 'File path to read' }
      },
      required: ['path']
    }
  },
  {
    name: 'write_file',
    description: 'Write content to a file',
    inputSchema: {
      type: 'object',
      properties: {
        path: { type: 'string', description: 'File path to write' },
        content: { type: 'string', description: 'Content to write' }
      },
      required: ['path', 'content']
    }
  },
  {
    name: 'search_files',
    description: 'Search for files containing specific text',
    inputSchema: {
      type: 'object',
      properties: {
        directory: { type: 'string', description: 'Directory to search in' },
        query: { type: 'string', description: 'Text to search for' }
      },
      required: ['directory', 'query']
    }
  }
]

Why does the second approach work better? Because AI can:

  1. Understand each tool's purpose instantly

  1. Combine tools naturally for complex tasks

  2. Provide better error messages when something fails

  3. Learn which tool to use without ambiguity

  1. Understand each tool's purpose instantly

  1. Combine tools naturally for complex tasks

  2. Provide better error messages when something fails

  3. Learn which tool to use without ambiguity

rESPONSE TIME:

200 MS

low Error clarity:

“error”

“Invalid”

Combination Flexibility:

Not able to combine

rESPONSE TIME:

40 MS

high Error clarity:

“Missing field: note_id”

“Invalid date format”

Combination Flexibility:

Able to combine

rESPONSE TIME:

200 MS

low Error clarity:

“error”

“Invalid”

Combination Flexibility:

Not able to combine

rESPONSE TIME:

40 MS

high Error clarity:

“Missing field: note_id”

“Invalid date format”

Combination Flexibility:

Able to combine

rESPONSE TIME:

200 MS

low Error clarity:

“error”

“Invalid”

Combination Flexibility:

Not able to combine

rESPONSE TIME:

40 MS

high Error clarity:

“Missing field: note_id”

“Invalid date format”

Combination Flexibility:

Able to combine

7.1 Tool Design Workshop

7.1 Tool Design Workshop

The Anatomy of a Great Tool

The Anatomy of a Great Tool

Let's dissect what makes a tool genuinely useful by building a real-world example: a Git analytics tool.

Let's dissect what makes a tool genuinely useful by building a real-world example: a Git analytics tool.

Git analytics server

Git analytics server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// git-analytics-server.js - A tool that helps understand your coding patterns
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { simpleGit } from 'simple-git';
import path from 'path';

class GitAnalytics {
  constructor(repoPath) {
    this.git = simpleGit(repoPath);
    this.repoPath = repoPath;
  }
  
  // Tool 1: Analyze commit patterns
  async analyzeCommitPatterns(days = 30) {
    const since = new Date();
    since.setDate(since.getDate() - days);
    
    const log = await this.git.log({
      since: since.toISOString(),
      '--stat': true
    });
    
    // Group commits by hour of day and day of week
    const patterns = {
      byHour: new Array(24).fill(0),
      byDayOfWeek: new Array(7).fill(0),
      byAuthor: {},
      totalCommits: log.all.length,
      averageFilesChanged: 0,
      mostActiveFiles: {}
    };
    
    let totalFiles = 0;
    
    for (const commit of log.all) {
      const date = new Date(commit.date);
      patterns.byHour[date.getHours()]++;
      patterns.byDayOfWeek[date.getDay()]++;
      
      // Track by author
      const author = commit.author_name;
      patterns.byAuthor[author] = (patterns.byAuthor[author] || 0) + 1;
      
      // Parse file changes from stat
      if (commit.diff && commit.diff.files) {
        totalFiles += commit.diff.files.length;
        
        for (const file of commit.diff.files) {
          patterns.mostActiveFiles[file.file] = 
            (patterns.mostActiveFiles[file.file] || 0) + 1;
        }
      }
    }
    
    patterns.averageFilesChanged = totalFiles / log.all.length;
    
    // Sort most active files
    patterns.mostActiveFiles = Object.entries(patterns.mostActiveFiles)
      .sort(([,a], [,b]) => b - a)
      .slice(0, 10)
      .reduce((obj, [file, count]) => {
        obj[file] = count;
        return obj;
      }, {});
    
    return patterns;
  }
  
  // Tool 2: Find code hotspots (files that change together)
  async findHotspots(threshold = 0.7) {
    const log = await this.git.log({ 
      '--stat': true,
      '-n': 1000  // Last 1000 commits
    });
    
    // Build co-occurrence matrix
    const coOccurrence = {};
    
    for (const commit of log.all) {
      if (!commit.diff?.files || commit.diff.files.length < 2) continue;
      
      const files = commit.diff.files.map(f => f.file);
      
      // For each pair of files in this commit
      for (let i = 0; i < files.length; i++) {
        for (let j = i + 1; j < files.length; j++) {
          const pair = [files[i], files[j]].sort().join('::');
          coOccurrence[pair] = (coOccurrence[pair] || 0) + 1;
        }
      }
    }
    
    // Find files that change together frequently
    const hotspots = Object.entries(coOccurrence)
      .filter(([_, count]) => count >= threshold * 10)  // At least 7 times for default threshold
      .sort(([,a], [,b]) => b - a)
      .slice(0, 20)
      .map(([pair, count]) => {
        const [file1, file2] = pair.split('::');
        return { file1, file2, count, correlation: count / 100 };
      });
    
    return hotspots;
  }
  
  // Tool 3: Generate productivity insights
  async getProductivityInsights() {
    const patterns = await this.analyzeCommitPatterns(90);  // Last 90 days
    
    const insights = [];
    
    // Most productive hour
    const maxHour = patterns.byHour.indexOf(Math.max(...patterns.byHour));
    insights.push({
      type: 'productive_time',
      insight: `Most productive hour: ${maxHour}:00-${maxHour+1}:00`,
      data: { hour: maxHour, commits: patterns.byHour[maxHour] }
    });
    
    // Work-life balance
    const weekendCommits = patterns.byDayOfWeek[0] + patterns.byDayOfWeek[6];
    const weekdayCommits = patterns.totalCommits - weekendCommits;
    const weekendRatio = weekendCommits / patterns.totalCommits;
    
    if (weekendRatio > 0.3) {
      insights.push({
        type: 'work_life_balance',
        insight: `${(weekendRatio * 100).toFixed(1)}% of commits on weekends - consider taking breaks!`,
        data: { weekendRatio, weekendCommits, weekdayCommits }
      });
    }
    
    // File focus
    const topFiles = Object.entries(patterns.mostActiveFiles).slice(0, 3);
    insights.push({
      type: 'file_focus',
      insight: `Top focus areas: ${topFiles.map(([f]) => path.basename(f)).join(', ')}`,
      data: { files: topFiles }
    });
    
    // Team collaboration
    const authorCount = Object.keys(patterns.byAuthor).length;
    if (authorCount > 1) {
      const topContributor = Object.entries(patterns.byAuthor)
        .sort(([,a], [,b]) => b - a)[0];
      insights.push({
        type: 'collaboration',
        insight: `${authorCount} contributors, led by ${topContributor[0]} (${topContributor[1]} commits)`,
        data: { authorCount, topContributor }
      });
    }
    
    return insights;
  }
}

// MCP Server setup
const server = new Server(
  {
    name: 'git-analytics',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Initialize GitAnalytics with current directory or specified path
const repoPath = process.argv[2] || process.cwd();
const analytics = new GitAnalytics(repoPath);

// Tool handlers
server.setRequestHandler('tools/call', async (request) => {
  const { name, arguments: args } = request.params;
  
  try {
    switch (name) {
      case 'analyze_commit_patterns': {
        const patterns = await analytics.analyzeCommitPatterns(args.days || 30);
        
        // Format response for readability
        const response = `# Commit Pattern Analysis (Last ${args.days || 30} days)

## Summary
- Total commits: ${patterns.totalCommits}
- Average files per commit: ${patterns.averageFilesChanged.toFixed(1)}
- Contributors: ${Object.keys(patterns.byAuthor).length}

## Most Active Hours
${patterns.byHour.map((count, hour) => 
  count > 0 ? `${hour}:00 - ${count} commits ${'█'.repeat(Math.ceil(count/5))}` : ''
).filter(Boolean).join('\n')}

## By Day of Week
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day, i) => 
  `${day}: ${patterns.byDayOfWeek[i]} commits`
).join('\n')}

## Most Active Files
${Object.entries(patterns.mostActiveFiles)
  .map(([file, count]) => `- ${file}: ${count} changes`)
  .join('\n')}`;
        
        return {
          content: [{
            type: 'text',
            text: response
          }]
        };
      }
      
      case 'find_code_hotspots': {
        const hotspots = await analytics.findHotspots(args.threshold || 0.7);
        
        const response = hotspots.length > 0
          ? `# Code Hotspots (Files that change together)

${hotspots.map(h => `## ${path.basename(h.file1)} ↔ ${path.basename(h.file2)}
- Changed together ${h.count} times
- Correlation: ${(h.correlation * 100).toFixed(1)}%
- Files: ${h.file1} and ${h.file2}`).join('\n\n')}

💡 Consider: Are these files tightly coupled? Could they be refactored?`
          : 'No significant code hotspots found. Your code has good separation of concerns!';
        
        return {
          content: [{
            type: 'text',
            text: response
          }]
        };
      }
      
      case 'get_productivity_insights': {
        const insights = await analytics.getProductivityInsights();
        
        const response = `# Git Productivity Insights

${insights.map(insight => {
  switch(insight.type) {
    case 'productive_time':
      return `🕐 **Peak Productivity**: ${insight.insight}`;
    case 'work_life_balance':
      return `⚖️ **Work-Life Balance**: ${insight.insight}`;
    case 'file_focus':
      return `🎯 **Focus Areas**: ${insight.insight}`;
    case 'collaboration':
      return `👥 **Team Dynamics**: ${insight.insight}`;
    default:
      return `💡 ${insight.insight}`;
  }
}).join('\n\n')}`;
        
        return {
          content: [{
            type: 'text',
            text: response
          }]
        };
      }
      
      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    // Helpful error messages
    if (error.message.includes('does not exist')) {
      return {
        content: [{
          type: 'text',
          text: `Error: Not a git repository. Please run this server in a git repository directory.`
        }]
      };
    }
    throw error;
  }
});

// Tool definitions with clear schemas
server.setRequestHandler('tools/list', async () => {
  return {
    tools: [
      {
        name: 'analyze_commit_patterns',
        description: 'Analyze git commit patterns to understand coding habits',
        inputSchema: {
          type: 'object',
          properties: {
            days: {
              type: 'number',
              description: 'Number of days to analyze (default: 30)',
              minimum: 1,
              maximum: 365
            }
          }
        }
      },
      {
        name: 'find_code_hotspots',
        description: 'Find files that frequently change together (potential coupling)',
        inputSchema: {
          type: 'object',
          properties: {
            threshold: {
              type: 'number',
              description: 'Correlation threshold 0-1 (default: 0.7)',
              minimum: 0.5,
              maximum: 1.0
            }
          }
        }
      },
      {
        name: 'get_productivity_insights',
        description: 'Get personalized insights about coding productivity and patterns',
        inputSchema: {
          type: 'object',
          properties: {}
        }
      }
    ]
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Git Analytics Server running for repository: ${repoPath}`);

This tool demonstrates several key principles:

This tool demonstrates several key principles:

Clear Purpose

Each tool does one specific thing

Rich Output

Returns formatted, actionable information

Smart Defaults

Works without parameters but accepts customization

Error Guidance

Helpful messages when things go wrong

Insights, Not Data

Interprets data to provide value

Heatmap

>>

Spikes at 10am–1pm

>>

Spikes at 8pm–11pm

Heatmap

>>

Spikes at 10am–1pm

>>

Spikes at 8pm–11pm

Heatmap

>>

Spikes at 10am–1pm

>>

Spikes at 8pm–11pm

File Relationship Graph

>>

Frequent co-change

>>

12 commits last month

File Relationship Graph

>>

Frequent co-change

>>

12 commits last month

File Relationship Graph

>>

Frequent co-change

>>

12 commits last month

Productivity insights

>>

70% commits on week

>>

You - 120 commits

Productivity insights

>>

70% commits on week

>>

You - 120 commits

Productivity insights

>>

70% commits on week

>>

You - 120 commits

The Goldilocks Principle

The Goldilocks Principle

Tools can be too simple or too complex. Finding "just right" is an art:

Tools can be too simple or too complex. Finding "just right" is an art:

SImple

SImple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ❌ Too Simple - Not useful enough
{
  name: 'get_time',
  description: 'Get current time'
  // Why: Claude already knows the time
}

// ❌ Too Complex - Cognitive overload  
{
  name: 'analyze_project',
  description: 'Analyze project code, dependencies, performance, security, tests, docs...',
  inputSchema: {
    // 50+ parameters
  }
  // Why: Too many responsibilities, unclear what it actually does
}

// ✅ Just Right - Clear purpose, composable
{
  name: 'analyze_dependencies',
  description: 'Analyze project dependencies for security and update status',
  inputSchema: {
    type: 'object',
    properties: {
      includeDevDependencies: {
        type: 'boolean',
        description: 'Include dev dependencies in analysis',
        default: false
      },
      securityOnly: {
        type: 'boolean', 
        description: 'Only report security issues',
        default: false
      }
    }
  }
  // Why: Specific purpose, clear options, composable with other tools
}

Bookmark Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// bookmark-manager.js - Thoughtfully designed bookmark tool
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import Database from 'better-sqlite3';
import fetch from 'node-fetch';
import { JSDOM } from 'jsdom';
import path from 'path';
import os from 'os';

class BookmarkManager {
  constructor() {
    // Store in user's home directory
    const dbPath = path.join(os.homedir(), '.mcp-bookmarks.db');
    this.db = new Database(dbPath);
    this.initDatabase();
  }
  
  initDatabase() {
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS bookmarks (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        url TEXT UNIQUE NOT NULL,
        title TEXT,
        description TEXT,
        tags TEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        accessed_count INTEGER DEFAULT 0,
        last_accessed DATETIME,
        is_archived BOOLEAN DEFAULT 0
      )
    `);
  }
  
  // Smart extraction of metadata
  async extractMetadata(url) {
    try {
      const response = await fetch(url, { timeout: 5000 });
      const html = await response.text();
      const dom = new JSDOM(html);
      const doc = dom.window.document;
      
      // Try various metadata sources
      const title = 
        doc.querySelector('meta[property="og:title"]')?.content ||
        doc.querySelector('title')?.textContent ||
        new URL(url).hostname;
        
      const description =
        doc.querySelector('meta[property="og:description"]')?.content ||
        doc.querySelector('meta[name="description"]')?.content ||
        '';
      
      // Auto-generate tags from URL and content
      const autoTags = [];
      const urlParts = new URL(url).hostname.split('.');
      autoTags.push(urlParts[urlParts.length - 2]); // Domain name
      
      if (url.includes('github.com')) autoTags.push('code', 'github');
      if (url.includes('youtube.com')) autoTags.push('video', 'youtube');
      if (url.includes('arxiv.org')) autoTags.push('research', 'paper');
      
      return {
        title: title.trim().substring(0, 200),
        description: description.trim().substring(0, 500),
        suggestedTags: autoTags
      };
    } catch (error) {
      console.error('Failed to extract metadata:', error);
      return {
        title: new URL(url).hostname,
        description: '',
        suggestedTags: []
      };
    }
  }
  
  // Tool implementations
  async addBookmark(url, { title, description, tags = [] } = {}) {
    // Extract metadata if not provided
    if (!title || !description) {
      const metadata = await this.extractMetadata(url);
      title = title || metadata.title;
      description = description || metadata.description;
      tags = [...tags, ...metadata.suggestedTags];
    }
    
    try {
      const stmt = this.db.prepare(`
        INSERT INTO bookmarks (url, title, description, tags)
        VALUES (?, ?, ?, ?)
      `);
      
      const result = stmt.run(
        url,
        title,
        description,
        [...new Set(tags)].join(',')  // Unique tags
      );
      
      return {
        id: result.lastInsertRowid,
        url,
        title,
        description,
        tags: [...new Set(tags)]
      };
    } catch (error) {
      if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
        // Update existing bookmark
        const updateStmt = this.db.prepare(`
          UPDATE bookmarks 
          SET accessed_count = accessed_count + 1,
              last_accessed = CURRENT_TIMESTAMP
          WHERE url = ?
        `);
        updateStmt.run(url);
        
        const existing = this.db.prepare('SELECT * FROM bookmarks WHERE url = ?').get(url);
        return { ...existing, tags: existing.tags.split(','), updated: true };
      }
      throw error;
    }
  }
  
  searchBookmarks({ query, tags = [], limit = 20 }) {
    let sql = `
      SELECT * FROM bookmarks 
      WHERE is_archived = 0
    `;
    const params = [];
    
    if (query) {
      sql += ` AND (title LIKE ? OR description LIKE ? OR url LIKE ?)`;
      const searchTerm = `%${query}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    if (tags.length > 0) {
      const tagConditions = tags.map(() => `tags LIKE ?`).join(' AND ');
      sql += ` AND (${tagConditions})`;
      tags.forEach(tag => params.push(`%${tag}%`));
    }
    
    sql += ` ORDER BY accessed_count DESC, created_at DESC LIMIT ?`;
    params.push(limit);
    
    const results = this.db.prepare(sql).all(...params);
    
    return results.map(bookmark => ({
      ...bookmark,
      tags: bookmark.tags ? bookmark.tags.split(',') : []
    }));
  }
  
  getSmartSuggestions() {
    // Get recently accessed
    const recent = this.db.prepare(`
      SELECT * FROM bookmarks
      WHERE last_accessed IS NOT NULL
      ORDER BY last_accessed DESC
      LIMIT 5
    `).all();
    
    // Get frequently accessed
    const frequent = this.db.prepare(`
      SELECT * FROM bookmarks
      WHERE accessed_count > 2
      ORDER BY accessed_count DESC
      LIMIT 5
    `).all();
    
    // Get by time of day
    const hour = new Date().getHours();
    const timeBasedTags = hour < 12 ? ['news', 'morning'] : 
                         hour < 17 ? ['work', 'reference'] :
                         ['entertainment', 'leisure'];
    
    const timeBased = this.db.prepare(`
      SELECT * FROM bookmarks
      WHERE ${timeBasedTags.map(() => 'tags LIKE ?').join(' OR ')}
      LIMIT 3
    `).all(...timeBasedTags.map(tag => `%${tag}%`));
    
    return {
      recent: recent.map(b => ({ ...b, tags: b.tags?.split(',') || [] })),
      frequent: frequent.map(b => ({ ...b, tags: b.tags?.split(',') || [] })),
      recommended: timeBased.map(b => ({ ...b, tags: b.tags?.split(',') || [] }))
    };
  }
}

// MCP Server setup
const server = new Server(
  {
    name: 'bookmark-manager',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

const bookmarks = new BookmarkManager();

server.setRequestHandler('tools/call', async (request) => {
  const { name, arguments: args } = request.params;
  
  switch (name) {
    case 'add_bookmark': {
      const result = await bookmarks.addBookmark(args.url, {
        title: args.title,
        description: args.description,
        tags: args.tags
      });
      
      return {
        content: [{
          type: 'text',
          text: result.updated 
            ? `Updated existing bookmark: ${result.title}`
            : `Added bookmark: ${result.title}\nTags: ${result.tags.join(', ')}`
        }]
      };
    }
    
    case 'search_bookmarks': {
      const results = bookmarks.searchBookmarks({
        query: args.query,
        tags: args.tags,
        limit: args.limit
      });
      
      if (results.length === 0) {
        return {
          content: [{
            type: 'text',
            text: 'No bookmarks found matching your search.'
          }]
        };
      }
      
      const response = results.map(b => 
        `**${b.title}**\n${b.url}\n${b.description || 'No description'}\nTags: ${b.tags.join(', ')}\n`
      ).join('\n---\n\n');
      
      return {
        content: [{
          type: 'text',
          text: `Found ${results.length} bookmarks:\n\n${response}`
        }]
      };
    }
    
    case 'get_bookmark_suggestions': {
      const suggestions = bookmarks.getSmartSuggestions();
      
      const response = `# Bookmark Suggestions

## Recently Used
${suggestions.recent.map(b => `- **${b.title}** - ${b.url}`).join('\n') || 'None yet'}

## Frequently Accessed  
${suggestions.frequent.map(b => `- **${b.title}** (${b.accessed_count} times)`).join('\n') || 'None yet'}

## Recommended for This Time
${suggestions.recommended.map(b => `- **${b.title}** - ${b.tags.join(', ')}`).join('\n') || 'None yet'}`;
      
      return {
        content: [{
          type: 'text',
          text: response
        }]
      };
    }
    
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

server.setRequestHandler('tools/list', async () => {
  return {
    tools: [
      {
        name: 'add_bookmark',
        description: 'Add or update a bookmark with automatic metadata extraction',
        inputSchema: {
          type: 'object',
          properties: {
            url: {
              type: 'string',
              description: 'URL to bookmark'
            },
            title: {
              type: 'string',
              description: 'Optional title (auto-extracted if not provided)'
            },
            description: {
              type: 'string',
              description: 'Optional description (auto-extracted if not provided)'
            },
            tags: {
              type: 'array',
              items: { type: 'string' },
              description: 'Tags for categorization'
            }
          },
          required: ['url']
        }
      },
      {
        name: 'search_bookmarks',
        description: 'Search bookmarks by text or tags',
        inputSchema: {
          type: 'object',
          properties: {
            query: {
              type: 'string',
              description: 'Search in title, description, and URL'
            },
            tags: {
              type: 'array',
              items: { type: 'string' },
              description: 'Filter by tags (AND operation)'
            },
            limit: {
              type: 'number',
              description: 'Maximum results to return',
              default: 20
            }
          }
        }
      },
      {
        name: 'get_bookmark_suggestions',
        description: 'Get smart bookmark suggestions based on usage patterns',
        inputSchema: {
          type: 'object',
          properties: {}
        }
      }
    ]
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Bookmark Manager Server running...');

Not too simple

Auto-extracts metadata, tracks usage, provides suggestions

Not too complex

Just three focused tools that work together

Just right

Solves a real problem with thoughtful features

Not too complex

Not too simple

Just right.

Not too complex

Not too simple

Just right.

Not too complex

Not too simple

Just right.

7.2 MCP Principle

7.2 MCP Principle

The Composition Symphony

The Composition Symphony

Great tools work better together. Let's see how our tools can orchestrate:

Great tools work better together. Let's see how our tools can orchestrate:

Connect CLaude

Connect CLaude

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Example: A week in review using multiple tools

You: Can you give me a summary of my week?

Claude: I'll analyze your week across different areas. Let me gather information
from your various tools.

[Claude uses git-analytics to check coding activity]
[Claude uses bookmark-manager to see what you've been reading]
[Claude uses note-keeper to review your notes]
[Claude uses calendar tool to check meetings]

Here's your week in review:

## 💻 Coding Activity
You had a productive week with 47 commits across 3 projects:
- Peak productivity was Wednesday 2-4 PM (12 commits)
- Main focus: Refactoring the authentication module
- You worked with Sarah on 3 pull requests

## 📚 Learning & Research  
You bookmarked 15 articles, primarily about:
- Rust async programming (5 articles)
- System design patterns (4 articles)
- MCP development (6 articles)

Most visited: "Understanding Rust Lifetimes" (accessed 4 times)

## 📝 Notes & Ideas
You created 8 notes this week:
- 3 about the authentication refactor
- 2 brainstorming session notes
- 3 learning notes about Rust

Key insight from your notes: "Consider using JWT refresh tokens
for better security"

## 📅 Time Management
- 12 meetings (18 hours total)
- Longest focused coding session: Thursday morning (3.5 hours)
- You maintained good work-life balance with no weekend commits

Would you like me to help you plan next week based on these patterns?

7.3 Composition Playground

7.3 Composition Playground

This is the power of composition - simple tools combining to create rich insights.

This is the power of composition - simple tools combining to create rich insights.

Error Handling: Failing Gracefully

Error Handling: Failing Gracefully

The difference between a good tool and a great tool is how it fails:

The difference between a good tool and a great tool is how it fails:

Performance Metrics

Performance Metrics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Error handling patterns for MCP tools

class ToolError extends Error {
  constructor(message, code, suggestions = []) {
    super(message);
    this.code = code;
    this.suggestions = suggestions;
  }
}

// Pattern 1: Validate inputs early
function validateUrl(url) {
  try {
    const parsed = new URL(url);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      throw new ToolError(
        'URL must use HTTP or HTTPS protocol',
        'INVALID_PROTOCOL',
        [`Try: https://${parsed.host}${parsed.pathname}`]
      );
    }
    return parsed;
  } catch (error) {
    if (error instanceof ToolError) throw error;
    
    throw new ToolError(
      'Invalid URL format',
      'INVALID_URL',
      [
        'Make sure URL includes protocol (https://)',
        'Check for special characters that need encoding',
        `Example: https://example.com/page`
      ]
    );
  }
}

// Pattern 2: Graceful degradation
async function fetchWithFallback(url, options = {}) {
  try {
    const response = await fetch(url, { 
      timeout: 5000,
      ...options 
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return await response.text();
  } catch (error) {
    // Try alternative approaches
    if (error.message.includes('timeout')) {
      // Try with longer timeout
      try {
        const response = await fetch(url, { 
          timeout: 15000,
          ...options 
        });
        return await response.text();
      } catch {
        throw new ToolError(
          'Website is not responding',
          'TIMEOUT',
          [
            'Check if the website is online',
            'Try again in a few moments',
            'Consider using cached data if available'
          ]
        );
      }
    }
    
    if (error.message.includes('ENOTFOUND')) {
      throw new ToolError(
        'Website not found',
        'NOT_FOUND',
        [
          'Check the URL for typos',
          'Verify the website exists',
          `Try searching for: ${new URL(url).hostname}`
        ]
      );
    }
    
    throw error;
  }
}

// Pattern 3: Progressive enhancement
async function analyzeText(text, options = {}) {
  const results = {
    basic: null,
    advanced: null,
    premium: null
  };
  
  // Always provide basic analysis
  results.basic = {
    wordCount: text.split(/\s+/).length,
    charCount: text.length,
    lineCount: text.split('\n').length
  };
  
  // Try advanced analysis
  try {
    if (options.includeReadability) {
      results.advanced = await calculateReadability(text);
    }
  } catch (error) {
    console.warn('Advanced analysis failed:', error);
    // Continue with basic results
  }
  
  // Try premium features if available
  try {
    if (options.includeSentiment && hasSentimentAPI()) {
      results.premium = await analyzeSentiment(text);
    }
  } catch (error) {
    console.warn('Premium analysis unavailable:', error);
  }
  
  return results;
}

// Pattern 4: Helpful error responses
server.setRequestHandler('tools/call', async (request) => {
  try {
    return await handleToolCall(request);
  } catch (error) {
    if (error instanceof ToolError) {
      // Structured error response
      return {
        content: [{
          type: 'text',
          text: `❌ ${error.message}\n\n` +
                `Error code: ${error.code}\n\n` +
                `💡 Suggestions:\n${error.suggestions.map(s => `- ${s}`).join('\n')}`
        }]
      };
    }
    
    // Unexpected errors - still helpful
    console.error('Unexpected error:', error);
    
    return {
      content: [{
        type: 'text',
        text: `❌ An unexpected error occurred: ${error.message}\n\n` +
              `💡 Try:\n` +
              `- Checking your input parameters\n` +
              `- Ensuring the server has necessary permissions\n` +
              `- Reviewing the server logs for details`
      }]
    };
  }
});

unauthorized

Close

unauthorized

Close

unauthorized

Close

7.4 Error Scenario Simulator

7.4 Error Scenario Simulator

Performance: Speed Matters

Performance: Speed Matters

Let's look at real performance optimization for MCP tools:

Let's look at real performance optimization for MCP tools:

Performance Responsive

Performance Responsive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Performance patterns for responsive tools

// Pattern 1: Streaming responses for long operations
async function* analyzeDirectory(dirPath) {
  const files = await fs.readdir(dirPath);
  
  yield {
    type: 'progress',
    message: `Found ${files.length} files to analyze`
  };
  
  let processed = 0;
  
  for (const file of files) {
    const filePath = path.join(dirPath, file);
    const stats = await fs.stat(filePath);
    
    if (stats.isFile()) {
      const analysis = await analyzeFile(filePath);
      
      processed++;
      yield {
        type: 'result',
        file: file,
        analysis: analysis,
        progress: processed / files.length
      };
    }
  }
  
  yield {
    type: 'complete',
    total: processed
  };
}

// Pattern 2: Intelligent caching
class CachedAnalyzer {
  constructor() {
    this.cache = new Map();
    this.cacheStats = { hits: 0, misses: 0 };
  }
  
  getCacheKey(filePath, mtime) {
    return `${filePath}:${mtime}`;
  }
  
  async analyzeWithCache(filePath) {
    const stats = await fs.stat(filePath);
    const cacheKey = this.getCacheKey(filePath, stats.mtime.getTime());
    
    // Check cache
    if (this.cache.has(cacheKey)) {
      this.cacheStats.hits++;
      return this.cache.get(cacheKey);
    }
    
    this.cacheStats.misses++;
    
    // Perform analysis
    const result = await this.performAnalysis(filePath);
    
    // Cache result
    this.cache.set(cacheKey, result);
    
    // Limit cache size
    if (this.cache.size > 1000) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    return result;
  }
}

// Pattern 3: Batch operations
class BatchProcessor {
  constructor(batchSize = 10, delayMs = 100) {
    this.batchSize = batchSize;
    this.delayMs = delayMs;
    this.queue = [];
    this.processing = false;
  }
  
  async add(item) {
    this.queue.push(item);
    
    if (!this.processing) {
      this.processing = true;
      setTimeout(() => this.processBatch(), this.delayMs);
    }
  }
  
  async processBatch() {
    const batch = this.queue.splice(0, this.batchSize);
    
    if (batch.length > 0) {
      // Process batch in parallel
      const results = await Promise.all(
        batch.map(item => this.processItem(item))
      );
      
      // Notify results
      batch.forEach((item, i) => {
        item.resolve(results[i]);
      });
    }
    
    if (this.queue.length > 0) {
      setTimeout(() => this.processBatch(), this.delayMs);
    } else {
      this.processing = false;
    }
  }
}

// Pattern 4: Early termination
async function searchWithEarlyTermination(query, options = {}) {
  const maxResults = options.maxResults || 10;
  const timeLimit = options.timeLimit || 5000;
  
  const startTime = Date.now();
  const results = [];
  
  for await (const result of searchGenerator(query)) {
    results.push(result);
    
    // Check termination conditions
    if (results.length >= maxResults) {
      break;
    }
    
    if (Date.now() - startTime > timeLimit) {
      console.warn(`Search time limit exceeded, returning ${results.length} results`);
      break;
    }
  }
  
  return results;
}

7.5 Performance Lab

7.5 Performance Lab

Testing Your Tools

Testing Your Tools

Great tools have great tests:

Great tools have great tests:

Testing PAttern

Testing PAttern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Testing patterns for MCP tools
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestClient } from '@modelcontextprotocol/sdk/testing';

describe('GitAnalytics Tool', () => {
  let client;
  let testRepoPath;
  
  beforeEach(async () => {
    // Create test repository
    testRepoPath = await createTestRepo();
    
    // Create test client
    client = new TestClient({
      command: 'node',
      args: ['./git-analytics-server.js', testRepoPath]
    });
    
    await client.start();
  });
  
  afterEach(async () => {
    await client.stop();
    await cleanupTestRepo(testRepoPath);
  });
  
  describe('analyze_commit_patterns', () => {
    it('should analyze commit patterns correctly', async () => {
      // Create test commits
      await createTestCommits(testRepoPath, [
        { message: 'Initial commit', date: '2024-03-01T09:00:00Z' },
        { message: 'Add feature', date: '2024-03-01T14:00:00Z' },
        { message: 'Fix bug', date: '2024-03-02T10:00:00Z' }
      ]);
      
      // Call tool
      const result = await client.callTool('analyze_commit_patterns', {
        days: 30
      });
      
      // Verify results
      expect(result.content[0].text).toContain('Total commits: 3');
      expect(result.content[0].text).toContain('9:00 - 1 commits');
      expect(result.content[0].text).toContain('14:00 - 1 commits');
    });
    
    it('should handle empty repository gracefully', async () => {
      const result = await client.callTool('analyze_commit_patterns', {
        days: 30
      });
      
      expect(result.content[0].text).toContain('Total commits: 0');
    });
  });
  
  describe('error handling', () => {
    it('should provide helpful error for non-git directory', async () => {
      // Point to non-git directory
      const nonGitClient = new TestClient({
        command: 'node',
        args: ['./git-analytics-server.js', '/tmp']
      });
      
      await nonGitClient.start();
      
      const result = await nonGitClient.callTool('analyze_commit_patterns');
      
      expect(result.content[0].text).toContain('Not a git repository');
      
      await nonGitClient.stop();
    });
  });
});

// Integration test example
describe('Multi-tool Integration', () => {
  it('should work with bookmark manager', async () => {
    // Start both servers
    const gitClient = await TestClient.start('./git-analytics-server.js');
    const bookmarkClient = await TestClient.start('./bookmark-manager.js');
    
    // Simulate workflow
    const gitInsights = await gitClient.callTool('get_productivity_insights');
    
    // Extract recommendation from insights
    const recommendation = extractRecommendation(gitInsights);
    
    // Save as bookmark
    const bookmark = await bookmarkClient.callTool('add_bookmark', {
      url: 'https://example.com/productivity-tips',
      title: 'Productivity Tips',
      tags: ['productivity', 'automated']
    });
    
    expect(bookmark.content[0].text).toContain('Added bookmark');
  });
});

Get immediate feedback

Run test

7.6 Test Workshop

7.6 Test Workshop

The Tool Maker's Mindset

The Tool Maker's Mindset

Let's be honest—things don't always work the first time. Here's how to debug MCP connections:

Let's be honest—things don't always work the first time. Here's how to debug MCP connections:

1. Start with the conversation

1. Start with the conversation

User

I wish I could...

I wish I could...

Your tool

Now you can!

Now you can!

2. Design for discovery

2. Design for discovery

1
2
3
4
5
6
7
8
9
// Bad: Generic names
'process_data'
'handle_request' 
'do_operation'

// Good: Specific, discoverable names
'analyze_code_complexity'
'generate_test_cases'
'optimize_image_size'

3. Embrace constraints

3. Embrace constraints

  1. Each tool should do ONE thing well

  2. Clear inputs produce clear outputs

  3. Errors should guide, not frustrate

  1. Each tool should do ONE thing well

  2. Clear inputs produce clear outputs

  3. Errors should guide, not frustrate

4. Think in workflows

4. Think in workflows

  1. How will this tool combine with others?

  2. What's the next logical step after using this tool?

  3. What context might the AI need?

  1. How will this tool combine with others?

  2. What's the next logical step after using this tool?

  3. What context might the AI need?

Scenario challenges

Solution #1

+210 Votes

Solution #2

+189 Votes

Solution #2

+105 Votes

Scenario challenges

Solution #1

+210 Votes

Solution #2

+189 Votes

Solution #2

+105 Votes

Scenario challenges

Solution #1

+210 Votes

Solution #2

+189 Votes

Solution #2

+105 Votes

Scenario challenges

Solution #1

+210 Votes

Solution #2

+189 Votes

Solution #2

+105 Votes

Scenario challenges

Solution #1

+210 Votes

Solution #2

+189 Votes

Solution #2

+105 Votes

7.7 Tool Design Challenge

7.7 Tool Design Challenge

Real Tools in Production

Real Tools in Production

Let's examine some production MCP tools and what makes them excellent

Let's examine some production MCP tools and what makes them excellent

File System

File System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// From the official MCP filesystem server
{
  name: 'read_file',
  description: 'Read the complete contents of a file',
  inputSchema: {
    type: 'object',
    properties: {
      path: {
        type: 'string',
        description: 'Path to the file to read'
      }
    },
    required: ['path']
  }
}

// Why it's good:
// - Crystal clear purpose
// - Minimal parameters
// - Obvious behavior
// - Composable with write_file, search_file, etc.

// From the Slack MCP server
{
  name: 'send_message', 
  description: 'Send a message to a Slack channel',
  inputSchema: {
    type: 'object',
    properties: {
      channel: {
        type: 'string',
        description: 'Channel name (without #) or channel ID'
      },
      text: {
        type: 'string',
        description: 'Message text (supports basic Slack markdown)'
      },
      thread_ts: {
        type: 'string',
        description: 'Thread timestamp to reply in thread (optional)'
      }
    },
    required: ['channel', 'text']
  }
}

Why it’s good:

Why it’s good:

  1. Covers 80% use case simply (channel + text)

  2. Optional parameter for advanced use (threads)

  3. Clear description of format expectations

  4. Returns useful data (timestamp, permalink)

  1. Covers 80% use case simply (channel + text)

  2. Optional parameter for advanced use (threads)

  3. Clear description of format expectations

  4. Returns useful data (timestamp, permalink)

Your Tool Portfolio

Your Tool Portfolio

As you build more tools, think about creating a cohesive portfolio:

As you build more tools, think about creating a cohesive portfolio:

My tool suite

My tool suite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// A well-designed tool portfolio example

const myToolSuite = {
  // Data Tools
  'csv-analyzer': 'Analyze CSV files for patterns and anomalies',
  'json-transformer': 'Transform JSON data between formats',
  'sql-query-builder': 'Build and execute safe SQL queries',
  
  // Productivity Tools
  'task-tracker': 'Track tasks with time estimates',
  'meeting-summarizer': 'Summarize meeting notes into actions',
  'email-drafter': 'Draft emails from bullet points',
  
  // Development Tools
  'code-reviewer': 'Review code for common issues',
  'dependency-analyzer': 'Analyze and update dependencies',
  'test-generator': 'Generate test cases from code',
  
  // Personal Tools
  'habit-tracker': 'Track daily habits and streaks',
  'expense-categorizer': 'Categorize expenses automatically',
  'recipe-suggester': 'Suggest recipes from available ingredients'
};

// These tools work together:
// - csv-analyzer → sql-query-builder (analyze then query)
// - meeting-summarizer → task-tracker (extract then track)
// - code-reviewer → test-generator (review then test)

7.8 Portfolio Builder

7.8 Portfolio Builder

The Future You're Building

The Future You're Building

Every tool you create expands what's possible with AI:

Every tool you create expands what's possible with AI:

Building MCP

Building MCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Today: Single-purpose tools
await mcp.callTool('read_file', { path: 'data.csv' });

// Tomorrow: Intelligent tool chains
await mcp.orchestrate('analyze business data and create report');
// MCP automatically:
// 1. Reads multiple data sources
// 2. Combines and analyzes
// 3. Generates visualizations
// 4. Creates report
// 5. Sends to stakeholders

// The future: Adaptive tools that learn
await mcp.evolve('improve my workflow');
// MCP observes your patterns and creates custom tools

old tools

Single-purpose

Intelligent tool chains

Adaptive tools

Now

Future

old tools

Single-purpose

Intelligent tool chains

Adaptive tools

Now

Future

old tools

Single-purpose

Intelligent tool chains

Adaptive tools

Now

Future

7.9 Tool Evolution Timeline

7.9 Tool Evolution Timeline