mcpeasy 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.claudeignore +0 -3
  3. data/.mcp.json +19 -1
  4. data/CHANGELOG.md +59 -0
  5. data/CLAUDE.md +19 -5
  6. data/README.md +19 -3
  7. data/lib/mcpeasy/cli.rb +62 -10
  8. data/lib/mcpeasy/config.rb +22 -1
  9. data/lib/mcpeasy/setup.rb +1 -0
  10. data/lib/mcpeasy/version.rb +1 -1
  11. data/lib/utilities/gcal/README.md +11 -3
  12. data/lib/utilities/gcal/cli.rb +110 -108
  13. data/lib/utilities/gcal/mcp.rb +463 -308
  14. data/lib/utilities/gcal/service.rb +312 -0
  15. data/lib/utilities/gdrive/README.md +3 -3
  16. data/lib/utilities/gdrive/cli.rb +98 -96
  17. data/lib/utilities/gdrive/mcp.rb +290 -288
  18. data/lib/utilities/gdrive/service.rb +293 -0
  19. data/lib/utilities/gmail/README.md +278 -0
  20. data/lib/utilities/gmail/cli.rb +264 -0
  21. data/lib/utilities/gmail/mcp.rb +846 -0
  22. data/lib/utilities/gmail/service.rb +547 -0
  23. data/lib/utilities/gmeet/cli.rb +131 -129
  24. data/lib/utilities/gmeet/mcp.rb +374 -372
  25. data/lib/utilities/gmeet/service.rb +411 -0
  26. data/lib/utilities/notion/README.md +287 -0
  27. data/lib/utilities/notion/cli.rb +245 -0
  28. data/lib/utilities/notion/mcp.rb +607 -0
  29. data/lib/utilities/notion/service.rb +327 -0
  30. data/lib/utilities/slack/README.md +3 -3
  31. data/lib/utilities/slack/cli.rb +69 -54
  32. data/lib/utilities/slack/mcp.rb +277 -226
  33. data/lib/utilities/slack/service.rb +134 -0
  34. data/mcpeasy.gemspec +6 -1
  35. metadata +87 -10
  36. data/env.template +0 -11
  37. data/lib/utilities/gcal/gcal_tool.rb +0 -308
  38. data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
  39. data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
  40. data/lib/utilities/slack/slack_tool.rb +0 -119
  41. data/logs/.keep +0 -0
@@ -0,0 +1,411 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "google/apis/calendar_v3"
5
+ require "googleauth"
6
+ require "signet/oauth_2/client"
7
+ require "fileutils"
8
+ require "json"
9
+ require "time"
10
+ require_relative "../_google/auth_server"
11
+ require_relative "../../mcpeasy/config"
12
+
13
+ module Gmeet
14
+ class Service
15
+ SCOPE = "https://www.googleapis.com/auth/calendar.readonly"
16
+
17
+ def initialize(skip_auth: false)
18
+ ensure_env!
19
+ @service = Google::Apis::CalendarV3::CalendarService.new
20
+ @service.authorization = authorize unless skip_auth
21
+ end
22
+
23
+ def list_meetings(start_date: nil, end_date: nil, max_results: 20, calendar_id: "primary")
24
+ # Default to today if no start date provided
25
+ now = Time.now
26
+ start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.new(now.year, now.month, now.day, 0, 0, 0)
27
+ # Default to 7 days from start if no end date provided
28
+ end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 7 * 24 * 60 * 60
29
+
30
+ results = @service.list_events(
31
+ calendar_id,
32
+ max_results: max_results,
33
+ single_events: true,
34
+ order_by: "startTime",
35
+ time_min: start_time.utc.iso8601,
36
+ time_max: end_time.utc.iso8601
37
+ )
38
+
39
+ # Filter for events that have Google Meet links
40
+ meetings = results.items.filter_map do |event|
41
+ meet_link = extract_meet_link(event)
42
+ next unless meet_link
43
+
44
+ {
45
+ id: event.id,
46
+ summary: event.summary,
47
+ description: event.description,
48
+ location: event.location,
49
+ start: format_event_time(event.start),
50
+ end: format_event_time(event.end),
51
+ attendees: format_attendees(event.attendees),
52
+ html_link: event.html_link,
53
+ meet_link: meet_link,
54
+ calendar_id: calendar_id
55
+ }
56
+ end
57
+
58
+ {meetings: meetings, count: meetings.length}
59
+ rescue Google::Apis::Error => e
60
+ raise "Google Calendar API Error: #{e.message}"
61
+ rescue => e
62
+ log_error("list_meetings", e)
63
+ raise e
64
+ end
65
+
66
+ def upcoming_meetings(max_results: 10, calendar_id: "primary")
67
+ # Get meetings starting from now
68
+ start_time = Time.now
69
+ # Look ahead 24 hours by default
70
+ end_time = start_time + 24 * 60 * 60
71
+
72
+ results = @service.list_events(
73
+ calendar_id,
74
+ max_results: max_results,
75
+ single_events: true,
76
+ order_by: "startTime",
77
+ time_min: start_time.utc.iso8601,
78
+ time_max: end_time.utc.iso8601
79
+ )
80
+
81
+ # Filter for events that have Google Meet links and are upcoming
82
+ meetings = results.items.filter_map do |event|
83
+ meet_link = extract_meet_link(event)
84
+ next unless meet_link
85
+
86
+ event_start_time = event.start.date_time&.to_time || (event.start.date ? Time.parse(event.start.date) : nil)
87
+ next unless event_start_time && event_start_time >= start_time
88
+
89
+ {
90
+ id: event.id,
91
+ summary: event.summary,
92
+ description: event.description,
93
+ location: event.location,
94
+ start: format_event_time(event.start),
95
+ end: format_event_time(event.end),
96
+ attendees: format_attendees(event.attendees),
97
+ html_link: event.html_link,
98
+ meet_link: meet_link,
99
+ calendar_id: calendar_id,
100
+ time_until_start: time_until_event(event_start_time)
101
+ }
102
+ end
103
+
104
+ {meetings: meetings, count: meetings.length}
105
+ rescue Google::Apis::Error => e
106
+ raise "Google Calendar API Error: #{e.message}"
107
+ rescue => e
108
+ log_error("upcoming_meetings", e)
109
+ raise e
110
+ end
111
+
112
+ def search_meetings(query, start_date: nil, end_date: nil, max_results: 10)
113
+ # Default to today if no start date provided
114
+ now = Time.now
115
+ start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.new(now.year, now.month, now.day, 0, 0, 0)
116
+ # Default to 30 days from start if no end date provided
117
+ end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 30 * 24 * 60 * 60
118
+
119
+ results = @service.list_events(
120
+ "primary",
121
+ q: query,
122
+ max_results: max_results,
123
+ single_events: true,
124
+ order_by: "startTime",
125
+ time_min: start_time.utc.iso8601,
126
+ time_max: end_time.utc.iso8601
127
+ )
128
+
129
+ # Filter for events that have Google Meet links
130
+ meetings = results.items.filter_map do |event|
131
+ meet_link = extract_meet_link(event)
132
+ next unless meet_link
133
+
134
+ {
135
+ id: event.id,
136
+ summary: event.summary,
137
+ description: event.description,
138
+ location: event.location,
139
+ start: format_event_time(event.start),
140
+ end: format_event_time(event.end),
141
+ attendees: format_attendees(event.attendees),
142
+ html_link: event.html_link,
143
+ meet_link: meet_link,
144
+ calendar_id: "primary"
145
+ }
146
+ end
147
+
148
+ {meetings: meetings, count: meetings.length}
149
+ rescue Google::Apis::Error => e
150
+ raise "Google Calendar API Error: #{e.message}"
151
+ rescue => e
152
+ log_error("search_meetings", e)
153
+ raise e
154
+ end
155
+
156
+ def get_meeting_url(event_id, calendar_id: "primary")
157
+ event = @service.get_event(calendar_id, event_id)
158
+ meet_link = extract_meet_link(event)
159
+
160
+ unless meet_link
161
+ raise "No Google Meet link found for this event"
162
+ end
163
+
164
+ {
165
+ event_id: event_id,
166
+ summary: event.summary,
167
+ meet_link: meet_link,
168
+ start: format_event_time(event.start),
169
+ end: format_event_time(event.end)
170
+ }
171
+ rescue Google::Apis::Error => e
172
+ raise "Google Calendar API Error: #{e.message}"
173
+ rescue => e
174
+ log_error("get_meeting_url", e)
175
+ raise e
176
+ end
177
+
178
+ def test_connection
179
+ calendar = @service.get_calendar("primary")
180
+ {
181
+ ok: true,
182
+ user: calendar.summary,
183
+ email: calendar.id
184
+ }
185
+ rescue Google::Apis::Error => e
186
+ raise "Google Calendar API Error: #{e.message}"
187
+ rescue => e
188
+ log_error("test_connection", e)
189
+ raise e
190
+ end
191
+
192
+ def authenticate
193
+ perform_auth_flow
194
+ {success: true}
195
+ rescue => e
196
+ {success: false, error: e.message}
197
+ end
198
+
199
+ def perform_auth_flow
200
+ client_id = Mcpeasy::Config.google_client_id
201
+ client_secret = Mcpeasy::Config.google_client_secret
202
+
203
+ unless client_id && client_secret
204
+ raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
205
+ end
206
+
207
+ # Create credentials using OAuth2 flow with localhost redirect
208
+ redirect_uri = "http://localhost:8080"
209
+ client = Signet::OAuth2::Client.new(
210
+ client_id: client_id,
211
+ client_secret: client_secret,
212
+ scope: SCOPE,
213
+ redirect_uri: redirect_uri,
214
+ authorization_uri: "https://accounts.google.com/o/oauth2/auth",
215
+ token_credential_uri: "https://oauth2.googleapis.com/token"
216
+ )
217
+
218
+ # Generate authorization URL
219
+ url = client.authorization_uri.to_s
220
+
221
+ puts "DEBUG: Client ID: #{client_id[0..20]}..."
222
+ puts "DEBUG: Scope: #{SCOPE}"
223
+ puts "DEBUG: Redirect URI: #{redirect_uri}"
224
+ puts
225
+
226
+ # Start callback server to capture OAuth code
227
+ puts "Starting temporary web server to capture OAuth callback..."
228
+ puts "Opening authorization URL in your default browser..."
229
+ puts url
230
+ puts
231
+
232
+ # Automatically open URL in default browser on macOS/Unix
233
+ if system("which open > /dev/null 2>&1")
234
+ system("open", url)
235
+ else
236
+ puts "Could not automatically open browser. Please copy the URL above manually."
237
+ end
238
+ puts
239
+ puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
240
+
241
+ # Wait for the authorization code with timeout
242
+ code = GoogleAuthServer.capture_auth_code
243
+
244
+ unless code
245
+ raise "Failed to receive authorization code. Please try again."
246
+ end
247
+
248
+ puts "✅ Authorization code received!"
249
+ client.code = code
250
+ client.fetch_access_token!
251
+
252
+ # Save credentials to config
253
+ credentials_data = {
254
+ client_id: client.client_id,
255
+ client_secret: client.client_secret,
256
+ scope: client.scope,
257
+ refresh_token: client.refresh_token,
258
+ access_token: client.access_token,
259
+ expires_at: client.expires_at
260
+ }
261
+
262
+ Mcpeasy::Config.save_google_token(credentials_data)
263
+ puts "✅ Authentication successful! Token saved to config"
264
+
265
+ client
266
+ rescue => e
267
+ log_error("perform_auth_flow", e)
268
+ raise "Authentication flow failed: #{e.message}"
269
+ end
270
+
271
+ private
272
+
273
+ def authorize
274
+ credentials_data = Mcpeasy::Config.google_token
275
+ unless credentials_data
276
+ raise <<~ERROR
277
+ Google Calendar authentication required!
278
+ Run the auth command first:
279
+ mcpz gmeet auth
280
+ ERROR
281
+ end
282
+
283
+ client = Signet::OAuth2::Client.new(
284
+ client_id: credentials_data.client_id,
285
+ client_secret: credentials_data.client_secret,
286
+ scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
287
+ refresh_token: credentials_data.refresh_token,
288
+ access_token: credentials_data.access_token,
289
+ token_credential_uri: "https://oauth2.googleapis.com/token"
290
+ )
291
+
292
+ # Check if token needs refresh
293
+ if credentials_data.expires_at
294
+ expires_at = if credentials_data.expires_at.is_a?(String)
295
+ Time.parse(credentials_data.expires_at)
296
+ else
297
+ Time.at(credentials_data.expires_at)
298
+ end
299
+
300
+ if Time.now >= expires_at
301
+ client.refresh!
302
+ # Update saved credentials with new access token
303
+ updated_data = {
304
+ client_id: credentials_data.client_id,
305
+ client_secret: credentials_data.client_secret,
306
+ scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
307
+ refresh_token: credentials_data.refresh_token,
308
+ access_token: client.access_token,
309
+ expires_at: client.expires_at
310
+ }
311
+ Mcpeasy::Config.save_google_token(updated_data)
312
+ end
313
+ end
314
+
315
+ client
316
+ rescue JSON::ParserError
317
+ raise "Invalid token data. Please re-run: mcpz gmeet auth"
318
+ rescue => e
319
+ log_error("authorize", e)
320
+ raise "Authentication failed: #{e.message}"
321
+ end
322
+
323
+ def ensure_env!
324
+ unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
325
+ raise <<~ERROR
326
+ Google API credentials not configured!
327
+ Please save your Google credentials.json file using:
328
+ mcpz config set_google_credentials <path_to_credentials.json>
329
+ ERROR
330
+ end
331
+ end
332
+
333
+ def extract_meet_link(event)
334
+ # Check various places where Google Meet links might be stored
335
+
336
+ # 1. Check conference data (most reliable)
337
+ if event.conference_data&.conference_solution&.name == "Google Meet"
338
+ return event.conference_data.entry_points&.find { |ep| ep.entry_point_type == "video" }&.uri
339
+ end
340
+
341
+ # 2. Check hangout link (legacy)
342
+ return event.hangout_link if event.hangout_link
343
+
344
+ # 3. Check description for meet.google.com links
345
+ if event.description
346
+ meet_match = event.description.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
347
+ return meet_match[0] if meet_match
348
+ end
349
+
350
+ # 4. Check location field
351
+ if event.location
352
+ meet_match = event.location.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
353
+ return meet_match[0] if meet_match
354
+ end
355
+
356
+ nil
357
+ end
358
+
359
+ def format_event_time(event_time)
360
+ return nil unless event_time
361
+
362
+ if event_time.date
363
+ # All-day event
364
+ {date: event_time.date}
365
+ elsif event_time.date_time
366
+ # Specific time event
367
+ {date_time: event_time.date_time.iso8601}
368
+ else
369
+ nil
370
+ end
371
+ end
372
+
373
+ def format_attendees(attendees)
374
+ return [] unless attendees
375
+
376
+ attendees.map do |attendee|
377
+ attendee.email
378
+ end.compact
379
+ end
380
+
381
+ def time_until_event(event_time)
382
+ now = Time.now
383
+ event_time = event_time.is_a?(String) ? Time.parse(event_time) : event_time
384
+
385
+ diff_seconds = (event_time - now).to_i
386
+ return "started" if diff_seconds < 0
387
+
388
+ if diff_seconds < 60
389
+ "#{diff_seconds} seconds"
390
+ elsif diff_seconds < 3600
391
+ "#{diff_seconds / 60} minutes"
392
+ elsif diff_seconds < 86400
393
+ hours = diff_seconds / 3600
394
+ minutes = (diff_seconds % 3600) / 60
395
+ "#{hours}h #{minutes}m"
396
+ else
397
+ days = diff_seconds / 86400
398
+ "#{days} days"
399
+ end
400
+ end
401
+
402
+ def log_error(method, error)
403
+ Mcpeasy::Config.ensure_config_dirs
404
+ File.write(
405
+ Mcpeasy::Config.log_file_path("gmeet", "error"),
406
+ "#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
407
+ mode: "a"
408
+ )
409
+ end
410
+ end
411
+ end
@@ -0,0 +1,287 @@
1
+ # Notion MCP Server
2
+
3
+ A Ruby-based Model Context Protocol (MCP) server that provides programmatic access to Notion's API. This server can operate in two modes:
4
+
5
+ 1. **CLI script**: Direct command-line usage for searching pages, databases, and retrieving content
6
+ 2. **MCP server**: Integration with AI assistants like Claude Code
7
+
8
+ ## Features
9
+
10
+ - 🔍 **Search pages** and databases in your Notion workspace
11
+ - 📄 **Get page details** including properties and metadata
12
+ - 📝 **Retrieve page content** as formatted text
13
+ - 🗃️ **Query databases** to find specific entries
14
+ - 👥 **List users** in your Notion workspace
15
+ - 👤 **Get user details** including email and avatar
16
+ - 🤖 **Get bot information** for the integration
17
+ - 🔐 **API key authentication** with secure credential storage
18
+ - 🛡️ **Error handling** with comprehensive API failure reporting
19
+ - ✅ **Connection testing** to verify authentication before operations
20
+
21
+ ## Prerequisites
22
+
23
+ ### 1. Ruby Environment
24
+
25
+ ```bash
26
+ # Install Ruby dependencies
27
+ bundle install
28
+ ```
29
+
30
+ Required gems:
31
+ - `standard` - Ruby code linting
32
+
33
+ ### 2. Notion Integration Setup
34
+
35
+ #### Step 1: Create a Notion Integration
36
+
37
+ 1. Go to https://www.notion.so/my-integrations
38
+ 2. Click **"New integration"**
39
+ 3. Give your integration a name (e.g., "MCPEasy Integration")
40
+ 4. Select the workspace you want to integrate with
41
+ 5. Click **"Submit"**
42
+ 6. Copy the **"Internal Integration Token"** (starts with `secret_`)
43
+
44
+ #### Step 2: Grant Database/Page Access
45
+
46
+ Your integration needs explicit access to pages and databases:
47
+
48
+ 1. **For databases**: Open the database → Click "..." → "Add connections" → Select your integration
49
+ 2. **For pages**: Open the page → Click "..." → "Add connections" → Select your integration
50
+
51
+ **Important**: The integration can only access content you explicitly share with it.
52
+
53
+ #### Step 3: Configure Credentials
54
+
55
+ Configure your Notion API key using the gem's configuration system:
56
+
57
+ ```bash
58
+ mcpz notion set_api_key secret_your-actual-notion-token
59
+ ```
60
+
61
+ This will store your API key securely in `~/.config/mcpeasy/notion.json`.
62
+
63
+ ## Usage
64
+
65
+ ### CLI Mode
66
+
67
+ #### Test Connection
68
+ ```bash
69
+ mcpz notion test
70
+ ```
71
+
72
+ #### Search Pages
73
+ ```bash
74
+ # Search all pages
75
+ mcpz notion search_pages
76
+
77
+ # Search with query
78
+ mcpz notion search_pages "meeting notes"
79
+
80
+ # Limit results
81
+ mcpz notion search_pages "project" --limit 5
82
+ ```
83
+
84
+ #### Search Databases
85
+ ```bash
86
+ # Search all databases
87
+ mcpz notion search_databases
88
+
89
+ # Search with query
90
+ mcpz notion search_databases "tasks"
91
+
92
+ # Limit results
93
+ mcpz notion search_databases "calendar" --limit 3
94
+ ```
95
+
96
+ #### Get Page Details
97
+ ```bash
98
+ # Get page metadata and properties
99
+ mcpz notion get_page PAGE_ID
100
+ ```
101
+
102
+ #### Get Page Content
103
+ ```bash
104
+ # Get the text content of a page
105
+ mcpz notion get_content PAGE_ID
106
+ ```
107
+
108
+ #### Query Database Entries
109
+ ```bash
110
+ # Get entries from a database
111
+ mcpz notion query_database DATABASE_ID
112
+
113
+ # Limit results
114
+ mcpz notion query_database DATABASE_ID --limit 20
115
+ ```
116
+
117
+ #### User Management
118
+ ```bash
119
+ # List all users in workspace
120
+ mcpz notion list_users
121
+
122
+ # Get details for a specific user
123
+ mcpz notion get_user USER_ID
124
+
125
+ # Get bot integration details
126
+ mcpz notion bot_info
127
+ ```
128
+
129
+ ### MCP Server Mode
130
+
131
+ #### Configuration for Claude Code
132
+
133
+ Add to your `.mcp.json` configuration:
134
+
135
+ ```json
136
+ {
137
+ "mcpServers": {
138
+ "notion": {
139
+ "command": "mcpz",
140
+ "args": ["notion", "mcp"]
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ #### Run as Standalone MCP Server
147
+
148
+ ```bash
149
+ mcpz notion mcp
150
+ ```
151
+
152
+ The server provides these tools to Claude Code:
153
+
154
+ - **test_connection**: Test Notion API connectivity
155
+ - **search_pages**: Search for pages in your workspace
156
+ - **search_databases**: Find databases in your workspace
157
+ - **get_page**: Get detailed information about a specific page
158
+ - **get_page_content**: Retrieve the text content of a page
159
+ - **query_database**: Query entries within a specific database
160
+ - **list_users**: List all users in the workspace
161
+ - **get_user**: Get details of a specific user
162
+ - **get_bot_user**: Get information about the integration bot
163
+
164
+ ## Security & Permissions
165
+
166
+ ### Required Notion Permissions
167
+
168
+ Your integration needs to be added to each page/database you want to access:
169
+
170
+ - **Read content**: Required for all operations
171
+ - **No additional permissions needed**: This integration is read-only
172
+
173
+ ### Local File Storage
174
+
175
+ - **Credentials**: Stored in `~/.config/mcpeasy/notion.json`
176
+ - **Logs**: Application logs for debugging
177
+
178
+ ### Best Practices
179
+
180
+ 1. **Never commit** API keys to version control
181
+ 2. **Grant minimal access** - only share necessary pages/databases with the integration
182
+ 3. **Regular rotation** of API keys (recommended annually)
183
+ 4. **Monitor usage** through Notion's integration settings
184
+
185
+ ## Troubleshooting
186
+
187
+ ### Common Issues
188
+
189
+ #### "Authentication failed" Error
190
+ - Check that your API key is correct and starts with `secret_`
191
+ - Re-run: `mcpz notion set_api_key secret_your-actual-notion-token`
192
+ - Verify the integration hasn't been deleted or disabled
193
+ - Ensure you're using an "Internal Integration Token", not a public OAuth token
194
+
195
+ #### "Access forbidden" Error
196
+ - The integration doesn't have access to the requested resource
197
+ - Add the integration to the specific page or database:
198
+ - Open page/database → "..." menu → "Add connections" → Select your integration
199
+
200
+ #### "Resource not found" Error
201
+ - Verify the page/database ID is correct
202
+ - Ensure the integration has been granted access to that specific resource
203
+ - Check that the page/database hasn't been deleted
204
+
205
+ #### "Rate limit exceeded" Error
206
+ - Notion has rate limits (3 requests per second)
207
+ - Wait a moment and try again
208
+ - The tool includes automatic retry logic for rate limits
209
+
210
+ ### Finding Page and Database IDs
211
+
212
+ 1. **From Notion URL**:
213
+ - Page: `https://notion.so/Page-Title-32alphanumeric` → ID is the 32-character string
214
+ - Database: `https://notion.so/database/32alphanumeric` → ID is the 32-character string
215
+
216
+ 2. **From search results**: Use `mcpz notion search_pages` or `mcpz notion search_databases`
217
+
218
+ ### Testing the Setup
219
+
220
+ 1. **Test CLI authentication**:
221
+ ```bash
222
+ mcpz notion test
223
+ ```
224
+
225
+ 2. **Test MCP server**:
226
+ ```bash
227
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | mcpz notion mcp
228
+ ```
229
+
230
+ 3. **Test basic search**:
231
+ ```bash
232
+ mcpz notion search_pages
233
+ ```
234
+
235
+ ## Development
236
+
237
+ ### File Structure
238
+
239
+ ```
240
+ lib/utilities/notion/
241
+ ├── cli.rb # Thor-based CLI interface
242
+ ├── mcp.rb # MCP server implementation
243
+ ├── service.rb # Notion API wrapper
244
+ └── README.md # This file
245
+ ```
246
+
247
+ ### Adding New Features
248
+
249
+ 1. **New API methods**: Add to `Service` class
250
+ 2. **New CLI commands**: Add to `CLI` class
251
+ 3. **New MCP tools**: Add to `MCPServer` class
252
+
253
+ ### API Coverage
254
+
255
+ Currently implemented Notion API endpoints:
256
+ - `/search` - Search pages and databases
257
+ - `/pages/{id}` - Get page details
258
+ - `/blocks/{id}/children` - Get page content blocks
259
+ - `/databases/{id}/query` - Query database entries
260
+ - `/users` - List all users in workspace
261
+ - `/users/{id}` - Get specific user details
262
+ - `/users/me` - Get bot user information and test authentication
263
+
264
+ ### Testing
265
+
266
+ ```bash
267
+ # Run Ruby linting
268
+ bundle exec standardrb
269
+
270
+ # Test CLI commands
271
+ mcpz notion test
272
+ mcpz notion search_pages
273
+
274
+ # Test MCP server manually
275
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | mcpz notion mcp
276
+ ```
277
+
278
+ ## Contributing
279
+
280
+ 1. Follow existing code patterns and style
281
+ 2. Add comprehensive error handling
282
+ 3. Update this README for new features
283
+ 4. Test both CLI and MCP modes
284
+
285
+ ## License
286
+
287
+ This project follows the same license as the parent repository.