mcpeasy 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.claudeignore +0 -3
- data/.mcp.json +10 -1
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +33 -10
- data/lib/mcpeasy/config.rb +22 -1
- data/lib/mcpeasy/setup.rb +1 -0
- data/lib/mcpeasy/version.rb +1 -1
- data/lib/utilities/gcal/README.md +11 -3
- data/lib/utilities/gcal/cli.rb +110 -108
- data/lib/utilities/gcal/mcp.rb +463 -308
- data/lib/utilities/gcal/service.rb +312 -0
- data/lib/utilities/gdrive/README.md +3 -3
- data/lib/utilities/gdrive/cli.rb +98 -96
- data/lib/utilities/gdrive/mcp.rb +290 -288
- data/lib/utilities/gdrive/service.rb +293 -0
- data/lib/utilities/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +409 -0
- data/lib/utilities/notion/README.md +287 -0
- data/lib/utilities/notion/cli.rb +245 -0
- data/lib/utilities/notion/mcp.rb +607 -0
- data/lib/utilities/notion/service.rb +327 -0
- data/lib/utilities/slack/README.md +3 -3
- data/lib/utilities/slack/cli.rb +69 -54
- data/lib/utilities/slack/mcp.rb +277 -226
- data/lib/utilities/slack/service.rb +134 -0
- metadata +11 -8
- data/env.template +0 -11
- data/lib/utilities/gcal/gcal_tool.rb +0 -308
- data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
- data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
- data/lib/utilities/slack/slack_tool.rb +0 -119
- data/logs/.keep +0 -0
@@ -0,0 +1,409 @@
|
|
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
|
+
start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
|
26
|
+
# Default to 7 days from start if no end date provided
|
27
|
+
end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 7 * 24 * 60 * 60
|
28
|
+
|
29
|
+
results = @service.list_events(
|
30
|
+
calendar_id,
|
31
|
+
max_results: max_results,
|
32
|
+
single_events: true,
|
33
|
+
order_by: "startTime",
|
34
|
+
time_min: start_time.utc.iso8601,
|
35
|
+
time_max: end_time.utc.iso8601
|
36
|
+
)
|
37
|
+
|
38
|
+
# Filter for events that have Google Meet links
|
39
|
+
meetings = results.items.filter_map do |event|
|
40
|
+
meet_link = extract_meet_link(event)
|
41
|
+
next unless meet_link
|
42
|
+
|
43
|
+
{
|
44
|
+
id: event.id,
|
45
|
+
summary: event.summary,
|
46
|
+
description: event.description,
|
47
|
+
location: event.location,
|
48
|
+
start: format_event_time(event.start),
|
49
|
+
end: format_event_time(event.end),
|
50
|
+
attendees: format_attendees(event.attendees),
|
51
|
+
html_link: event.html_link,
|
52
|
+
meet_link: meet_link,
|
53
|
+
calendar_id: calendar_id
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
{meetings: meetings, count: meetings.length}
|
58
|
+
rescue Google::Apis::Error => e
|
59
|
+
raise "Google Calendar API Error: #{e.message}"
|
60
|
+
rescue => e
|
61
|
+
log_error("list_meetings", e)
|
62
|
+
raise e
|
63
|
+
end
|
64
|
+
|
65
|
+
def upcoming_meetings(max_results: 10, calendar_id: "primary")
|
66
|
+
# Get meetings starting from now
|
67
|
+
start_time = Time.now
|
68
|
+
# Look ahead 24 hours by default
|
69
|
+
end_time = start_time + 24 * 60 * 60
|
70
|
+
|
71
|
+
results = @service.list_events(
|
72
|
+
calendar_id,
|
73
|
+
max_results: max_results,
|
74
|
+
single_events: true,
|
75
|
+
order_by: "startTime",
|
76
|
+
time_min: start_time.utc.iso8601,
|
77
|
+
time_max: end_time.utc.iso8601
|
78
|
+
)
|
79
|
+
|
80
|
+
# Filter for events that have Google Meet links and are upcoming
|
81
|
+
meetings = results.items.filter_map do |event|
|
82
|
+
meet_link = extract_meet_link(event)
|
83
|
+
next unless meet_link
|
84
|
+
|
85
|
+
event_start_time = event.start.date_time&.to_time || (event.start.date ? Time.parse(event.start.date) : nil)
|
86
|
+
next unless event_start_time && event_start_time >= start_time
|
87
|
+
|
88
|
+
{
|
89
|
+
id: event.id,
|
90
|
+
summary: event.summary,
|
91
|
+
description: event.description,
|
92
|
+
location: event.location,
|
93
|
+
start: format_event_time(event.start),
|
94
|
+
end: format_event_time(event.end),
|
95
|
+
attendees: format_attendees(event.attendees),
|
96
|
+
html_link: event.html_link,
|
97
|
+
meet_link: meet_link,
|
98
|
+
calendar_id: calendar_id,
|
99
|
+
time_until_start: time_until_event(event_start_time)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
{meetings: meetings, count: meetings.length}
|
104
|
+
rescue Google::Apis::Error => e
|
105
|
+
raise "Google Calendar API Error: #{e.message}"
|
106
|
+
rescue => e
|
107
|
+
log_error("upcoming_meetings", e)
|
108
|
+
raise e
|
109
|
+
end
|
110
|
+
|
111
|
+
def search_meetings(query, start_date: nil, end_date: nil, max_results: 10)
|
112
|
+
# Default to today if no start date provided
|
113
|
+
start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
|
114
|
+
# Default to 30 days from start if no end date provided
|
115
|
+
end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 30 * 24 * 60 * 60
|
116
|
+
|
117
|
+
results = @service.list_events(
|
118
|
+
"primary",
|
119
|
+
q: query,
|
120
|
+
max_results: max_results,
|
121
|
+
single_events: true,
|
122
|
+
order_by: "startTime",
|
123
|
+
time_min: start_time.utc.iso8601,
|
124
|
+
time_max: end_time.utc.iso8601
|
125
|
+
)
|
126
|
+
|
127
|
+
# Filter for events that have Google Meet links
|
128
|
+
meetings = results.items.filter_map do |event|
|
129
|
+
meet_link = extract_meet_link(event)
|
130
|
+
next unless meet_link
|
131
|
+
|
132
|
+
{
|
133
|
+
id: event.id,
|
134
|
+
summary: event.summary,
|
135
|
+
description: event.description,
|
136
|
+
location: event.location,
|
137
|
+
start: format_event_time(event.start),
|
138
|
+
end: format_event_time(event.end),
|
139
|
+
attendees: format_attendees(event.attendees),
|
140
|
+
html_link: event.html_link,
|
141
|
+
meet_link: meet_link,
|
142
|
+
calendar_id: "primary"
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
{meetings: meetings, count: meetings.length}
|
147
|
+
rescue Google::Apis::Error => e
|
148
|
+
raise "Google Calendar API Error: #{e.message}"
|
149
|
+
rescue => e
|
150
|
+
log_error("search_meetings", e)
|
151
|
+
raise e
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_meeting_url(event_id, calendar_id: "primary")
|
155
|
+
event = @service.get_event(calendar_id, event_id)
|
156
|
+
meet_link = extract_meet_link(event)
|
157
|
+
|
158
|
+
unless meet_link
|
159
|
+
raise "No Google Meet link found for this event"
|
160
|
+
end
|
161
|
+
|
162
|
+
{
|
163
|
+
event_id: event_id,
|
164
|
+
summary: event.summary,
|
165
|
+
meet_link: meet_link,
|
166
|
+
start: format_event_time(event.start),
|
167
|
+
end: format_event_time(event.end)
|
168
|
+
}
|
169
|
+
rescue Google::Apis::Error => e
|
170
|
+
raise "Google Calendar API Error: #{e.message}"
|
171
|
+
rescue => e
|
172
|
+
log_error("get_meeting_url", e)
|
173
|
+
raise e
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_connection
|
177
|
+
calendar = @service.get_calendar("primary")
|
178
|
+
{
|
179
|
+
ok: true,
|
180
|
+
user: calendar.summary,
|
181
|
+
email: calendar.id
|
182
|
+
}
|
183
|
+
rescue Google::Apis::Error => e
|
184
|
+
raise "Google Calendar API Error: #{e.message}"
|
185
|
+
rescue => e
|
186
|
+
log_error("test_connection", e)
|
187
|
+
raise e
|
188
|
+
end
|
189
|
+
|
190
|
+
def authenticate
|
191
|
+
perform_auth_flow
|
192
|
+
{success: true}
|
193
|
+
rescue => e
|
194
|
+
{success: false, error: e.message}
|
195
|
+
end
|
196
|
+
|
197
|
+
def perform_auth_flow
|
198
|
+
client_id = Mcpeasy::Config.google_client_id
|
199
|
+
client_secret = Mcpeasy::Config.google_client_secret
|
200
|
+
|
201
|
+
unless client_id && client_secret
|
202
|
+
raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Create credentials using OAuth2 flow with localhost redirect
|
206
|
+
redirect_uri = "http://localhost:8080"
|
207
|
+
client = Signet::OAuth2::Client.new(
|
208
|
+
client_id: client_id,
|
209
|
+
client_secret: client_secret,
|
210
|
+
scope: SCOPE,
|
211
|
+
redirect_uri: redirect_uri,
|
212
|
+
authorization_uri: "https://accounts.google.com/o/oauth2/auth",
|
213
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
214
|
+
)
|
215
|
+
|
216
|
+
# Generate authorization URL
|
217
|
+
url = client.authorization_uri.to_s
|
218
|
+
|
219
|
+
puts "DEBUG: Client ID: #{client_id[0..20]}..."
|
220
|
+
puts "DEBUG: Scope: #{SCOPE}"
|
221
|
+
puts "DEBUG: Redirect URI: #{redirect_uri}"
|
222
|
+
puts
|
223
|
+
|
224
|
+
# Start callback server to capture OAuth code
|
225
|
+
puts "Starting temporary web server to capture OAuth callback..."
|
226
|
+
puts "Opening authorization URL in your default browser..."
|
227
|
+
puts url
|
228
|
+
puts
|
229
|
+
|
230
|
+
# Automatically open URL in default browser on macOS/Unix
|
231
|
+
if system("which open > /dev/null 2>&1")
|
232
|
+
system("open", url)
|
233
|
+
else
|
234
|
+
puts "Could not automatically open browser. Please copy the URL above manually."
|
235
|
+
end
|
236
|
+
puts
|
237
|
+
puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
|
238
|
+
|
239
|
+
# Wait for the authorization code with timeout
|
240
|
+
code = GoogleAuthServer.capture_auth_code
|
241
|
+
|
242
|
+
unless code
|
243
|
+
raise "Failed to receive authorization code. Please try again."
|
244
|
+
end
|
245
|
+
|
246
|
+
puts "✅ Authorization code received!"
|
247
|
+
client.code = code
|
248
|
+
client.fetch_access_token!
|
249
|
+
|
250
|
+
# Save credentials to config
|
251
|
+
credentials_data = {
|
252
|
+
client_id: client.client_id,
|
253
|
+
client_secret: client.client_secret,
|
254
|
+
scope: client.scope,
|
255
|
+
refresh_token: client.refresh_token,
|
256
|
+
access_token: client.access_token,
|
257
|
+
expires_at: client.expires_at
|
258
|
+
}
|
259
|
+
|
260
|
+
Mcpeasy::Config.save_google_token(credentials_data)
|
261
|
+
puts "✅ Authentication successful! Token saved to config"
|
262
|
+
|
263
|
+
client
|
264
|
+
rescue => e
|
265
|
+
log_error("perform_auth_flow", e)
|
266
|
+
raise "Authentication flow failed: #{e.message}"
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def authorize
|
272
|
+
credentials_data = Mcpeasy::Config.google_token
|
273
|
+
unless credentials_data
|
274
|
+
raise <<~ERROR
|
275
|
+
Google Calendar authentication required!
|
276
|
+
Run the auth command first:
|
277
|
+
mcpz gmeet auth
|
278
|
+
ERROR
|
279
|
+
end
|
280
|
+
|
281
|
+
client = Signet::OAuth2::Client.new(
|
282
|
+
client_id: credentials_data.client_id,
|
283
|
+
client_secret: credentials_data.client_secret,
|
284
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
285
|
+
refresh_token: credentials_data.refresh_token,
|
286
|
+
access_token: credentials_data.access_token,
|
287
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
288
|
+
)
|
289
|
+
|
290
|
+
# Check if token needs refresh
|
291
|
+
if credentials_data.expires_at
|
292
|
+
expires_at = if credentials_data.expires_at.is_a?(String)
|
293
|
+
Time.parse(credentials_data.expires_at)
|
294
|
+
else
|
295
|
+
Time.at(credentials_data.expires_at)
|
296
|
+
end
|
297
|
+
|
298
|
+
if Time.now >= expires_at
|
299
|
+
client.refresh!
|
300
|
+
# Update saved credentials with new access token
|
301
|
+
updated_data = {
|
302
|
+
client_id: credentials_data.client_id,
|
303
|
+
client_secret: credentials_data.client_secret,
|
304
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
305
|
+
refresh_token: credentials_data.refresh_token,
|
306
|
+
access_token: client.access_token,
|
307
|
+
expires_at: client.expires_at
|
308
|
+
}
|
309
|
+
Mcpeasy::Config.save_google_token(updated_data)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
client
|
314
|
+
rescue JSON::ParserError
|
315
|
+
raise "Invalid token data. Please re-run: mcpz gmeet auth"
|
316
|
+
rescue => e
|
317
|
+
log_error("authorize", e)
|
318
|
+
raise "Authentication failed: #{e.message}"
|
319
|
+
end
|
320
|
+
|
321
|
+
def ensure_env!
|
322
|
+
unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
|
323
|
+
raise <<~ERROR
|
324
|
+
Google API credentials not configured!
|
325
|
+
Please save your Google credentials.json file using:
|
326
|
+
mcpz config set_google_credentials <path_to_credentials.json>
|
327
|
+
ERROR
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def extract_meet_link(event)
|
332
|
+
# Check various places where Google Meet links might be stored
|
333
|
+
|
334
|
+
# 1. Check conference data (most reliable)
|
335
|
+
if event.conference_data&.conference_solution&.name == "Google Meet"
|
336
|
+
return event.conference_data.entry_points&.find { |ep| ep.entry_point_type == "video" }&.uri
|
337
|
+
end
|
338
|
+
|
339
|
+
# 2. Check hangout link (legacy)
|
340
|
+
return event.hangout_link if event.hangout_link
|
341
|
+
|
342
|
+
# 3. Check description for meet.google.com links
|
343
|
+
if event.description
|
344
|
+
meet_match = event.description.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
|
345
|
+
return meet_match[0] if meet_match
|
346
|
+
end
|
347
|
+
|
348
|
+
# 4. Check location field
|
349
|
+
if event.location
|
350
|
+
meet_match = event.location.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
|
351
|
+
return meet_match[0] if meet_match
|
352
|
+
end
|
353
|
+
|
354
|
+
nil
|
355
|
+
end
|
356
|
+
|
357
|
+
def format_event_time(event_time)
|
358
|
+
return nil unless event_time
|
359
|
+
|
360
|
+
if event_time.date
|
361
|
+
# All-day event
|
362
|
+
{date: event_time.date}
|
363
|
+
elsif event_time.date_time
|
364
|
+
# Specific time event
|
365
|
+
{date_time: event_time.date_time.iso8601}
|
366
|
+
else
|
367
|
+
nil
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def format_attendees(attendees)
|
372
|
+
return [] unless attendees
|
373
|
+
|
374
|
+
attendees.map do |attendee|
|
375
|
+
attendee.email
|
376
|
+
end.compact
|
377
|
+
end
|
378
|
+
|
379
|
+
def time_until_event(event_time)
|
380
|
+
now = Time.now
|
381
|
+
event_time = event_time.is_a?(String) ? Time.parse(event_time) : event_time
|
382
|
+
|
383
|
+
diff_seconds = (event_time - now).to_i
|
384
|
+
return "started" if diff_seconds < 0
|
385
|
+
|
386
|
+
if diff_seconds < 60
|
387
|
+
"#{diff_seconds} seconds"
|
388
|
+
elsif diff_seconds < 3600
|
389
|
+
"#{diff_seconds / 60} minutes"
|
390
|
+
elsif diff_seconds < 86400
|
391
|
+
hours = diff_seconds / 3600
|
392
|
+
minutes = (diff_seconds % 3600) / 60
|
393
|
+
"#{hours}h #{minutes}m"
|
394
|
+
else
|
395
|
+
days = diff_seconds / 86400
|
396
|
+
"#{days} days"
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def log_error(method, error)
|
401
|
+
Mcpeasy::Config.ensure_config_dirs
|
402
|
+
File.write(
|
403
|
+
Mcpeasy::Config.log_file_path("gmeet", "error"),
|
404
|
+
"#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
|
405
|
+
mode: "a"
|
406
|
+
)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
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.
|