swarm-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv/
28
+
29
+ # IDE
30
+ .idea/
31
+ .vscode/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Testing
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
44
+
45
+ # Build
46
+ *.whl
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alex Priest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarm-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Foursquare Swarm check-in data
5
+ Project-URL: Homepage, https://github.com/alexpriest/swarm-mcp
6
+ Project-URL: Repository, https://github.com/alexpriest/swarm-mcp
7
+ Project-URL: Issues, https://github.com/alexpriest/swarm-mcp/issues
8
+ Author-email: Alex Priest <hello@alexpriest.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: anthropic,checkins,claude,foursquare,mcp,swarm
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: mcp>=1.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Swarm MCP Server
25
+
26
+ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that provides access to your [Foursquare Swarm](https://www.swarmapp.com/) check-in data. Use it with Claude Desktop, Claude Code, or any MCP-compatible client to analyze your check-in history.
27
+
28
+ ## Features
29
+
30
+ | Tool | Description |
31
+ |------|-------------|
32
+ | `get_checkins` | Get paginated check-in history |
33
+ | `get_checkins_by_date_range` | Get check-ins within a specific date range |
34
+ | `get_recent_checkins` | Get check-ins from the past X days |
35
+ | `get_checkin_details` | Get details about a specific check-in |
36
+ | `get_all_checkins` | Retrieve your entire check-in history |
37
+ | `get_checkin_stats` | Get statistics (total count, date range, averages) |
38
+ | `search_checkins` | Search check-ins by venue name or category |
39
+
40
+ ## Installation
41
+
42
+ ### Using uvx (recommended)
43
+
44
+ ```bash
45
+ uvx swarm-mcp
46
+ ```
47
+
48
+ ### Using pip
49
+
50
+ ```bash
51
+ pip install swarm-mcp
52
+ ```
53
+
54
+ ## Setup
55
+
56
+ ### 1. Get Your Foursquare Access Token
57
+
58
+ You'll need a Foursquare OAuth2 access token:
59
+
60
+ 1. Go to [Foursquare Developer Apps](https://foursquare.com/developers/apps)
61
+ 2. Create a new app (or use an existing one)
62
+ 3. Note your **Client ID** and **Client Secret**
63
+ 4. Generate an access token using the OAuth2 flow, or use the [API Explorer](https://docs.foursquare.com/developer/reference/v2-users-self) to get a token quickly
64
+
65
+ ### 2. Configure Your MCP Client
66
+
67
+ #### Claude Desktop
68
+
69
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "swarm": {
75
+ "command": "uvx",
76
+ "args": ["swarm-mcp"],
77
+ "env": {
78
+ "FOURSQUARE_TOKEN": "your-access-token-here"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ #### Claude Code
86
+
87
+ ```bash
88
+ claude mcp add swarm uvx swarm-mcp -e FOURSQUARE_TOKEN=your-access-token-here
89
+ ```
90
+
91
+ Or add manually to your config:
92
+
93
+ ```json
94
+ {
95
+ "Swarm": {
96
+ "command": "uvx",
97
+ "args": ["swarm-mcp"],
98
+ "env": {
99
+ "FOURSQUARE_TOKEN": "your-access-token-here"
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Usage Examples
106
+
107
+ Once configured, you can ask Claude things like:
108
+
109
+ - "Show me my recent Swarm check-ins"
110
+ - "How many times have I checked into coffee shops this year?"
111
+ - "What's my most visited venue?"
112
+ - "Show me all my check-ins in New York"
113
+ - "What are my check-in stats?"
114
+ - "Search my check-ins for 'airport'"
115
+
116
+ ## Example Output
117
+
118
+ ```
119
+ 📊 SWARM CHECK-IN STATS
120
+ =============================================
121
+ Total check-ins: 12,456
122
+ Years active: 10.2 years
123
+ Days active: 3,726
124
+ Avg check-ins/day: 3.34
125
+
126
+ 📅 First check-in: March 15, 2014 at Coffee Shop (NYC)
127
+ 📍 Most recent: Today at Office (San Francisco)
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ # Clone the repo
134
+ git clone https://github.com/alexpriest/swarm-mcp.git
135
+ cd swarm-mcp
136
+
137
+ # Install in development mode
138
+ pip install -e .
139
+
140
+ # Run the server
141
+ FOURSQUARE_TOKEN=your-token swarm-mcp
142
+ ```
143
+
144
+ ## API Reference
145
+
146
+ This server uses the [Foursquare API v2](https://docs.foursquare.com/developer/reference/checkins):
147
+ - [User Check-ins](https://docs.foursquare.com/developer/reference/v2-users-checkins)
148
+ - [Check-in Details](https://docs.foursquare.com/developer/reference/v2-checkins-details)
149
+
150
+ ## License
151
+
152
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,129 @@
1
+ # Swarm MCP Server
2
+
3
+ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that provides access to your [Foursquare Swarm](https://www.swarmapp.com/) check-in data. Use it with Claude Desktop, Claude Code, or any MCP-compatible client to analyze your check-in history.
4
+
5
+ ## Features
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `get_checkins` | Get paginated check-in history |
10
+ | `get_checkins_by_date_range` | Get check-ins within a specific date range |
11
+ | `get_recent_checkins` | Get check-ins from the past X days |
12
+ | `get_checkin_details` | Get details about a specific check-in |
13
+ | `get_all_checkins` | Retrieve your entire check-in history |
14
+ | `get_checkin_stats` | Get statistics (total count, date range, averages) |
15
+ | `search_checkins` | Search check-ins by venue name or category |
16
+
17
+ ## Installation
18
+
19
+ ### Using uvx (recommended)
20
+
21
+ ```bash
22
+ uvx swarm-mcp
23
+ ```
24
+
25
+ ### Using pip
26
+
27
+ ```bash
28
+ pip install swarm-mcp
29
+ ```
30
+
31
+ ## Setup
32
+
33
+ ### 1. Get Your Foursquare Access Token
34
+
35
+ You'll need a Foursquare OAuth2 access token:
36
+
37
+ 1. Go to [Foursquare Developer Apps](https://foursquare.com/developers/apps)
38
+ 2. Create a new app (or use an existing one)
39
+ 3. Note your **Client ID** and **Client Secret**
40
+ 4. Generate an access token using the OAuth2 flow, or use the [API Explorer](https://docs.foursquare.com/developer/reference/v2-users-self) to get a token quickly
41
+
42
+ ### 2. Configure Your MCP Client
43
+
44
+ #### Claude Desktop
45
+
46
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "swarm": {
52
+ "command": "uvx",
53
+ "args": ["swarm-mcp"],
54
+ "env": {
55
+ "FOURSQUARE_TOKEN": "your-access-token-here"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ #### Claude Code
63
+
64
+ ```bash
65
+ claude mcp add swarm uvx swarm-mcp -e FOURSQUARE_TOKEN=your-access-token-here
66
+ ```
67
+
68
+ Or add manually to your config:
69
+
70
+ ```json
71
+ {
72
+ "Swarm": {
73
+ "command": "uvx",
74
+ "args": ["swarm-mcp"],
75
+ "env": {
76
+ "FOURSQUARE_TOKEN": "your-access-token-here"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Usage Examples
83
+
84
+ Once configured, you can ask Claude things like:
85
+
86
+ - "Show me my recent Swarm check-ins"
87
+ - "How many times have I checked into coffee shops this year?"
88
+ - "What's my most visited venue?"
89
+ - "Show me all my check-ins in New York"
90
+ - "What are my check-in stats?"
91
+ - "Search my check-ins for 'airport'"
92
+
93
+ ## Example Output
94
+
95
+ ```
96
+ 📊 SWARM CHECK-IN STATS
97
+ =============================================
98
+ Total check-ins: 12,456
99
+ Years active: 10.2 years
100
+ Days active: 3,726
101
+ Avg check-ins/day: 3.34
102
+
103
+ 📅 First check-in: March 15, 2014 at Coffee Shop (NYC)
104
+ 📍 Most recent: Today at Office (San Francisco)
105
+ ```
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ # Clone the repo
111
+ git clone https://github.com/alexpriest/swarm-mcp.git
112
+ cd swarm-mcp
113
+
114
+ # Install in development mode
115
+ pip install -e .
116
+
117
+ # Run the server
118
+ FOURSQUARE_TOKEN=your-token swarm-mcp
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ This server uses the [Foursquare API v2](https://docs.foursquare.com/developer/reference/checkins):
124
+ - [User Check-ins](https://docs.foursquare.com/developer/reference/v2-users-checkins)
125
+ - [Check-in Details](https://docs.foursquare.com/developer/reference/v2-checkins-details)
126
+
127
+ ## License
128
+
129
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "swarm-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for Foursquare Swarm check-in data"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Alex Priest", email = "hello@alexpriest.com" }
10
+ ]
11
+ keywords = ["mcp", "foursquare", "swarm", "checkins", "claude", "anthropic"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ dependencies = [
22
+ "mcp>=1.0.0",
23
+ "httpx>=0.27.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/alexpriest/swarm-mcp"
28
+ Repository = "https://github.com/alexpriest/swarm-mcp"
29
+ Issues = "https://github.com/alexpriest/swarm-mcp/issues"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [project.scripts]
36
+ swarm-mcp = "swarm_mcp.server:main"
@@ -0,0 +1,3 @@
1
+ """MCP server for Foursquare Swarm check-in data."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server for Foursquare Swarm check-in data.
4
+
5
+ Requires FOURSQUARE_TOKEN environment variable with OAuth2 access token.
6
+ Get your token from: https://foursquare.com/developers/apps
7
+ """
8
+
9
+ import os
10
+ import asyncio
11
+ import json
12
+ from datetime import datetime, timedelta
13
+ from typing import Any
14
+
15
+ import httpx
16
+ from mcp.server import Server
17
+ from mcp.server.stdio import stdio_server
18
+ from mcp.types import Tool, TextContent
19
+
20
+ # Foursquare API configuration
21
+ API_BASE = "https://api.foursquare.com/v2"
22
+ API_VERSION = "20231010" # Foursquare requires a version date
23
+
24
+ server = Server("swarm-mcp")
25
+
26
+
27
+ def get_token() -> str:
28
+ """Get the Foursquare OAuth token from environment."""
29
+ token = os.environ.get("FOURSQUARE_TOKEN")
30
+ if not token:
31
+ raise ValueError(
32
+ "FOURSQUARE_TOKEN environment variable is required. "
33
+ "Get your token from https://foursquare.com/developers/apps"
34
+ )
35
+ return token
36
+
37
+
38
+ async def make_request(endpoint: str, params: dict = None) -> dict:
39
+ """Make an authenticated request to the Foursquare API."""
40
+ token = get_token()
41
+
42
+ url = f"{API_BASE}{endpoint}"
43
+ request_params = {
44
+ "oauth_token": token,
45
+ "v": API_VERSION,
46
+ **(params or {})
47
+ }
48
+
49
+ async with httpx.AsyncClient() as client:
50
+ response = await client.get(url, params=request_params, timeout=30.0)
51
+ response.raise_for_status()
52
+ return response.json()
53
+
54
+
55
+ @server.list_tools()
56
+ async def list_tools() -> list[Tool]:
57
+ """List available tools."""
58
+ return [
59
+ Tool(
60
+ name="get_checkins",
61
+ description="Get the authenticated user's check-in history. Returns check-ins with venue info, timestamps, and optional photos.",
62
+ inputSchema={
63
+ "type": "object",
64
+ "properties": {
65
+ "limit": {
66
+ "type": "integer",
67
+ "description": "Number of check-ins to return (max 250, default 50)",
68
+ "default": 50
69
+ },
70
+ "offset": {
71
+ "type": "integer",
72
+ "description": "Offset for pagination (default 0)",
73
+ "default": 0
74
+ },
75
+ "sort": {
76
+ "type": "string",
77
+ "description": "Sort order: 'newestfirst' or 'oldestfirst'",
78
+ "enum": ["newestfirst", "oldestfirst"],
79
+ "default": "newestfirst"
80
+ }
81
+ }
82
+ }
83
+ ),
84
+ Tool(
85
+ name="get_checkins_by_date_range",
86
+ description="Get check-ins within a specific date range.",
87
+ inputSchema={
88
+ "type": "object",
89
+ "properties": {
90
+ "start_date": {
91
+ "type": "string",
92
+ "description": "Start date in ISO format (YYYY-MM-DD)"
93
+ },
94
+ "end_date": {
95
+ "type": "string",
96
+ "description": "End date in ISO format (YYYY-MM-DD)"
97
+ },
98
+ "limit": {
99
+ "type": "integer",
100
+ "description": "Maximum number of check-ins to return (max 250)",
101
+ "default": 250
102
+ }
103
+ },
104
+ "required": ["start_date", "end_date"]
105
+ }
106
+ ),
107
+ Tool(
108
+ name="get_recent_checkins",
109
+ description="Get check-ins from the past X days.",
110
+ inputSchema={
111
+ "type": "object",
112
+ "properties": {
113
+ "days": {
114
+ "type": "integer",
115
+ "description": "Number of days to look back (default 7)",
116
+ "default": 7
117
+ },
118
+ "limit": {
119
+ "type": "integer",
120
+ "description": "Maximum number of check-ins to return",
121
+ "default": 50
122
+ }
123
+ }
124
+ }
125
+ ),
126
+ Tool(
127
+ name="get_checkin_details",
128
+ description="Get detailed information about a specific check-in by ID.",
129
+ inputSchema={
130
+ "type": "object",
131
+ "properties": {
132
+ "checkin_id": {
133
+ "type": "string",
134
+ "description": "The ID of the check-in to retrieve"
135
+ }
136
+ },
137
+ "required": ["checkin_id"]
138
+ }
139
+ ),
140
+ Tool(
141
+ name="get_all_checkins",
142
+ description="Get ALL check-ins by paginating through the entire history. Use with caution - may take time for users with many check-ins.",
143
+ inputSchema={
144
+ "type": "object",
145
+ "properties": {
146
+ "max_checkins": {
147
+ "type": "integer",
148
+ "description": "Maximum total check-ins to retrieve (default 1000, use -1 for unlimited)",
149
+ "default": 1000
150
+ }
151
+ }
152
+ }
153
+ ),
154
+ Tool(
155
+ name="get_checkin_stats",
156
+ description="Get statistics about your check-in history (total count, date range, etc.)",
157
+ inputSchema={
158
+ "type": "object",
159
+ "properties": {}
160
+ }
161
+ ),
162
+ Tool(
163
+ name="search_checkins",
164
+ description="Search check-ins by venue name or category.",
165
+ inputSchema={
166
+ "type": "object",
167
+ "properties": {
168
+ "query": {
169
+ "type": "string",
170
+ "description": "Search term to match against venue names"
171
+ },
172
+ "limit": {
173
+ "type": "integer",
174
+ "description": "Maximum results to return",
175
+ "default": 50
176
+ }
177
+ },
178
+ "required": ["query"]
179
+ }
180
+ )
181
+ ]
182
+
183
+
184
+ def format_checkin(checkin: dict) -> dict:
185
+ """Format a check-in for readable output."""
186
+ venue = checkin.get("venue", {})
187
+ location = venue.get("location", {})
188
+
189
+ # Get category
190
+ categories = venue.get("categories", [])
191
+ category = categories[0].get("name") if categories else "Unknown"
192
+
193
+ # Format timestamp
194
+ created_at = checkin.get("createdAt", 0)
195
+ dt = datetime.fromtimestamp(created_at)
196
+
197
+ return {
198
+ "id": checkin.get("id"),
199
+ "created_at": dt.isoformat(),
200
+ "venue": {
201
+ "name": venue.get("name", "Unknown"),
202
+ "category": category,
203
+ "address": location.get("formattedAddress", []),
204
+ "city": location.get("city"),
205
+ "state": location.get("state"),
206
+ "country": location.get("country"),
207
+ "lat": location.get("lat"),
208
+ "lng": location.get("lng")
209
+ },
210
+ "shout": checkin.get("shout"), # User's comment
211
+ "photos_count": checkin.get("photos", {}).get("count", 0),
212
+ "likes_count": checkin.get("likes", {}).get("count", 0),
213
+ "comments_count": checkin.get("comments", {}).get("count", 0)
214
+ }
215
+
216
+
217
+ @server.call_tool()
218
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
219
+ """Handle tool calls."""
220
+
221
+ try:
222
+ if name == "get_checkins":
223
+ limit = min(arguments.get("limit", 50), 250)
224
+ offset = arguments.get("offset", 0)
225
+ sort = arguments.get("sort", "newestfirst")
226
+
227
+ data = await make_request("/users/self/checkins", {
228
+ "limit": limit,
229
+ "offset": offset,
230
+ "sort": sort
231
+ })
232
+
233
+ checkins = data.get("response", {}).get("checkins", {})
234
+ total = checkins.get("count", 0)
235
+ items = [format_checkin(c) for c in checkins.get("items", [])]
236
+
237
+ result = {
238
+ "total_checkins": total,
239
+ "returned": len(items),
240
+ "offset": offset,
241
+ "checkins": items
242
+ }
243
+
244
+ elif name == "get_checkins_by_date_range":
245
+ start_date = datetime.fromisoformat(arguments["start_date"])
246
+ end_date = datetime.fromisoformat(arguments["end_date"])
247
+ limit = min(arguments.get("limit", 250), 250)
248
+
249
+ # Convert to timestamps
250
+ after_timestamp = int(start_date.timestamp())
251
+ before_timestamp = int((end_date + timedelta(days=1)).timestamp()) # Include end date
252
+
253
+ data = await make_request("/users/self/checkins", {
254
+ "limit": limit,
255
+ "afterTimestamp": after_timestamp,
256
+ "beforeTimestamp": before_timestamp,
257
+ "sort": "newestfirst"
258
+ })
259
+
260
+ checkins = data.get("response", {}).get("checkins", {})
261
+ items = [format_checkin(c) for c in checkins.get("items", [])]
262
+
263
+ result = {
264
+ "date_range": f"{arguments['start_date']} to {arguments['end_date']}",
265
+ "count": len(items),
266
+ "checkins": items
267
+ }
268
+
269
+ elif name == "get_recent_checkins":
270
+ days = arguments.get("days", 7)
271
+ limit = min(arguments.get("limit", 50), 250)
272
+
273
+ after_timestamp = int((datetime.now() - timedelta(days=days)).timestamp())
274
+
275
+ data = await make_request("/users/self/checkins", {
276
+ "limit": limit,
277
+ "afterTimestamp": after_timestamp,
278
+ "sort": "newestfirst"
279
+ })
280
+
281
+ checkins = data.get("response", {}).get("checkins", {})
282
+ items = [format_checkin(c) for c in checkins.get("items", [])]
283
+
284
+ result = {
285
+ "period": f"Last {days} days",
286
+ "count": len(items),
287
+ "checkins": items
288
+ }
289
+
290
+ elif name == "get_checkin_details":
291
+ checkin_id = arguments["checkin_id"]
292
+
293
+ data = await make_request(f"/checkins/{checkin_id}")
294
+ checkin = data.get("response", {}).get("checkin", {})
295
+
296
+ result = format_checkin(checkin)
297
+
298
+ # Add photos if available
299
+ photos = checkin.get("photos", {}).get("items", [])
300
+ if photos:
301
+ result["photos"] = [
302
+ {
303
+ "url": f"{p.get('prefix')}original{p.get('suffix')}",
304
+ "width": p.get("width"),
305
+ "height": p.get("height")
306
+ }
307
+ for p in photos
308
+ ]
309
+
310
+ elif name == "get_all_checkins":
311
+ max_checkins = arguments.get("max_checkins", 1000)
312
+ all_checkins = []
313
+ offset = 0
314
+ batch_size = 250
315
+
316
+ while True:
317
+ data = await make_request("/users/self/checkins", {
318
+ "limit": batch_size,
319
+ "offset": offset,
320
+ "sort": "newestfirst"
321
+ })
322
+
323
+ checkins = data.get("response", {}).get("checkins", {})
324
+ items = checkins.get("items", [])
325
+
326
+ if not items:
327
+ break
328
+
329
+ all_checkins.extend([format_checkin(c) for c in items])
330
+ offset += len(items)
331
+
332
+ if max_checkins > 0 and len(all_checkins) >= max_checkins:
333
+ all_checkins = all_checkins[:max_checkins]
334
+ break
335
+
336
+ if len(items) < batch_size:
337
+ break
338
+
339
+ result = {
340
+ "total_retrieved": len(all_checkins),
341
+ "checkins": all_checkins
342
+ }
343
+
344
+ elif name == "get_checkin_stats":
345
+ # Get first page to get total count
346
+ data = await make_request("/users/self/checkins", {
347
+ "limit": 1,
348
+ "sort": "newestfirst"
349
+ })
350
+
351
+ total = data.get("response", {}).get("checkins", {}).get("count", 0)
352
+ newest = data.get("response", {}).get("checkins", {}).get("items", [])
353
+
354
+ # Get oldest check-in
355
+ oldest_data = await make_request("/users/self/checkins", {
356
+ "limit": 1,
357
+ "sort": "oldestfirst"
358
+ })
359
+ oldest = oldest_data.get("response", {}).get("checkins", {}).get("items", [])
360
+
361
+ result = {
362
+ "total_checkins": total,
363
+ "newest_checkin": format_checkin(newest[0]) if newest else None,
364
+ "oldest_checkin": format_checkin(oldest[0]) if oldest else None,
365
+ }
366
+
367
+ if newest and oldest:
368
+ newest_dt = datetime.fromtimestamp(newest[0].get("createdAt", 0))
369
+ oldest_dt = datetime.fromtimestamp(oldest[0].get("createdAt", 0))
370
+ days_active = (newest_dt - oldest_dt).days
371
+ result["days_active"] = days_active
372
+ result["avg_checkins_per_day"] = round(total / max(days_active, 1), 2)
373
+
374
+ elif name == "search_checkins":
375
+ query = arguments["query"].lower()
376
+ limit = arguments.get("limit", 50)
377
+
378
+ # We need to fetch check-ins and filter locally
379
+ # The API doesn't support venue name search on check-ins
380
+ all_checkins = []
381
+ offset = 0
382
+ batch_size = 250
383
+
384
+ while len(all_checkins) < limit:
385
+ data = await make_request("/users/self/checkins", {
386
+ "limit": batch_size,
387
+ "offset": offset,
388
+ "sort": "newestfirst"
389
+ })
390
+
391
+ checkins = data.get("response", {}).get("checkins", {})
392
+ items = checkins.get("items", [])
393
+
394
+ if not items:
395
+ break
396
+
397
+ for item in items:
398
+ venue = item.get("venue", {})
399
+ venue_name = venue.get("name", "").lower()
400
+ categories = venue.get("categories", [])
401
+ category_names = [c.get("name", "").lower() for c in categories]
402
+
403
+ if query in venue_name or any(query in cat for cat in category_names):
404
+ all_checkins.append(format_checkin(item))
405
+ if len(all_checkins) >= limit:
406
+ break
407
+
408
+ offset += len(items)
409
+
410
+ if len(items) < batch_size:
411
+ break
412
+
413
+ # Safety limit to avoid infinite loops
414
+ if offset > 5000:
415
+ break
416
+
417
+ result = {
418
+ "query": arguments["query"],
419
+ "count": len(all_checkins),
420
+ "checkins": all_checkins
421
+ }
422
+
423
+ else:
424
+ result = {"error": f"Unknown tool: {name}"}
425
+
426
+ return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]
427
+
428
+ except httpx.HTTPStatusError as e:
429
+ error_msg = f"API error: {e.response.status_code}"
430
+ try:
431
+ error_detail = e.response.json()
432
+ error_msg += f" - {error_detail}"
433
+ except:
434
+ pass
435
+ return [TextContent(type="text", text=error_msg)]
436
+ except Exception as e:
437
+ return [TextContent(type="text", text=f"Error: {str(e)}")]
438
+
439
+
440
+ async def run_server():
441
+ """Run the MCP server with stdio transport."""
442
+ async with stdio_server() as (read_stream, write_stream):
443
+ await server.run(read_stream, write_stream, server.create_initialization_options())
444
+
445
+
446
+ def main():
447
+ """Run the MCP server."""
448
+ asyncio.run(run_server())
449
+
450
+
451
+ if __name__ == "__main__":
452
+ main()