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.
- swarm_mcp-0.1.0/.gitignore +46 -0
- swarm_mcp-0.1.0/LICENSE +21 -0
- swarm_mcp-0.1.0/PKG-INFO +152 -0
- swarm_mcp-0.1.0/README.md +129 -0
- swarm_mcp-0.1.0/pyproject.toml +36 -0
- swarm_mcp-0.1.0/src/swarm_mcp/__init__.py +3 -0
- swarm_mcp-0.1.0/src/swarm_mcp/server.py +452 -0
|
@@ -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
|
swarm_mcp-0.1.0/LICENSE
ADDED
|
@@ -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.
|
swarm_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -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,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()
|