@anotarakape/mcp-server 0.0.0-stage → 1.0.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.
- package/README.md +106 -2
- package/dist/index.js +270 -0
- package/package.json +30 -4
package/README.md
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ☕ TaraKape MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that allows developers and AI assistants (Claude Desktop, Cursor, Copilot, etc.) to securely query and discover coffee shops from the **TaraKape** database across Metro Manila and the Philippines.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏛️ Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Developers / AI Assistants (Cursor, Claude Desktop)
|
|
11
|
+
│ (MCP Protocol - stdio)
|
|
12
|
+
▼
|
|
13
|
+
[ @tarakape/mcp-server ]
|
|
14
|
+
│ (HTTPS with API Key)
|
|
15
|
+
▼
|
|
16
|
+
[ AWS API Gateway (REST) ]
|
|
17
|
+
├── API Key Validation & Usage Plans (Quotas/Throttling)
|
|
18
|
+
└── CloudWatch Monitoring
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
[ AWS Lambda Data Gateway ]
|
|
22
|
+
└── Fast In-Memory Geo & Tag Queries
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
[ TaraKape Cafe Database ]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🛠️ Tools Provided to AI Agents
|
|
31
|
+
|
|
32
|
+
| Tool | Description | Key Inputs |
|
|
33
|
+
| :--- | :--- | :--- |
|
|
34
|
+
| `search_cafes` | Search cafes by keywords, address, or amenities | `query`, `hasWifi`, `hasOutlets`, `isPetFriendly`, `vibe`, `limit` |
|
|
35
|
+
| `find_nearby_cafes` | Find cafes nearest to GPS coordinates | `latitude`, `longitude`, `radiusKm`, `limit` |
|
|
36
|
+
| `get_cafe_details` | Retrieve complete details, hours, photos for a cafe | `idOrSlug` |
|
|
37
|
+
| `list_vibe_tags` | List all valid atmosphere and mood tags | *(none)* |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🚀 Quick Setup for Developers
|
|
42
|
+
|
|
43
|
+
### In Claude Desktop (`claude_desktop_config.json`)
|
|
44
|
+
|
|
45
|
+
Add the following to your configuration file:
|
|
46
|
+
|
|
47
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
48
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"tarakape": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "@anotarakape/mcp-server"],
|
|
56
|
+
"env": {
|
|
57
|
+
"TARAKAPE_API_URL": "https://mcp.tarakape.cc",
|
|
58
|
+
"TARAKAPE_API_KEY": "YOUR_DEVELOPER_API_KEY"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### In Cursor (`.cursor/mcp.json`)
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"tarakape": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["-y", "@anotarakape/mcp-server"],
|
|
73
|
+
"env": {
|
|
74
|
+
"TARAKAPE_API_URL": "https://mcp.tarakape.cc",
|
|
75
|
+
"TARAKAPE_API_KEY": "YOUR_DEVELOPER_API_KEY"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ☁️ Deploying the AWS Gateway
|
|
85
|
+
|
|
86
|
+
### 1. Deploy the Lambda Gateway
|
|
87
|
+
1. Zip the files inside `lambda/`:
|
|
88
|
+
```bash
|
|
89
|
+
cd lambda
|
|
90
|
+
zip lambda.zip index.js package.json
|
|
91
|
+
```
|
|
92
|
+
2. In **AWS Console** → **Lambda** (e.g. Region `ap-southeast-1` Singapore or `us-east-1`):
|
|
93
|
+
- Function name: `tarakape-mcp-gateway`
|
|
94
|
+
- Runtime: **Node.js 20.x**
|
|
95
|
+
- Upload `lambda.zip` and click **Deploy**.
|
|
96
|
+
|
|
97
|
+
### 2. Put AWS API Gateway in Front
|
|
98
|
+
1. Go to **AWS API Gateway** → **Create API** → **REST API**.
|
|
99
|
+
2. Create resource `{proxy+}` with method `ANY` (or specific `POST /search`, `POST /nearby`, etc.).
|
|
100
|
+
3. Choose **Lambda Function** integration and select `tarakape-mcp-gateway`.
|
|
101
|
+
4. Enable **API Key Required** on methods.
|
|
102
|
+
5. Create a **Usage Plan**:
|
|
103
|
+
- Throttle: 10 requests/second, Burst: 20
|
|
104
|
+
- Quota: 10,000 requests per month
|
|
105
|
+
6. Generate an **API Key** for your external developers and link it to the Usage Plan.
|
|
106
|
+
7. Deploy the API to a stage (e.g. `prod`).
|
|
107
|
+
8. Copy the **Invoke URL** and provide it to developers.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
// Base URL for TaraKape API Gateway (defaults to https://mcp.tarakape.cc)
|
|
6
|
+
const API_URL = process.env.TARAKAPE_API_URL || "https://mcp.tarakape.cc";
|
|
7
|
+
const API_KEY = process.env.TARAKAPE_API_KEY || "";
|
|
8
|
+
const headers = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"User-Agent": "TaraKape-MCP-Server/1.0.0",
|
|
11
|
+
};
|
|
12
|
+
if (API_KEY) {
|
|
13
|
+
headers["x-api-key"] = API_KEY;
|
|
14
|
+
}
|
|
15
|
+
// Helper to make API requests to AWS API Gateway / TaraKape Backend
|
|
16
|
+
async function callGateway(path, method = "GET", body) {
|
|
17
|
+
let url = API_URL.replace(/\/+$/, "");
|
|
18
|
+
if (path && path !== "/") {
|
|
19
|
+
url += path.startsWith("/") ? path : `/${path}`;
|
|
20
|
+
}
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errorText = await response.text();
|
|
28
|
+
throw new Error(`Gateway returned HTTP ${response.status}: ${errorText}`);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
// Initialize MCP Server
|
|
33
|
+
const server = new McpServer({
|
|
34
|
+
name: "tarakape",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
});
|
|
37
|
+
const isActionGateway = !API_URL.endsWith("/cafes") ||
|
|
38
|
+
API_URL.includes("execute-api") ||
|
|
39
|
+
API_URL.includes("lambda-url") ||
|
|
40
|
+
API_URL.includes("mcp.tarakape.cc") ||
|
|
41
|
+
API_URL.includes("gateway") ||
|
|
42
|
+
API_URL.includes("localhost");
|
|
43
|
+
// Tool 1: Search Cafes
|
|
44
|
+
server.tool("search_cafes", "Search cafes across Metro Manila and the Philippines with optional filters for Wi-Fi, power outlets, pet friendliness, and aesthetic vibe.", {
|
|
45
|
+
query: z.string().optional().describe("Search keywords matching cafe name, city, or address"),
|
|
46
|
+
hasWifi: z.boolean().optional().describe("Filter only cafes with confirmed Wi-Fi"),
|
|
47
|
+
hasOutlets: z.boolean().optional().describe("Filter only cafes with power outlets for laptops"),
|
|
48
|
+
isPetFriendly: z.boolean().optional().describe("Filter only pet-friendly cafes"),
|
|
49
|
+
vibe: z.string().optional().describe("Filter by vibe tag (e.g. 'aesthetic', 'work-friendly', 'cozy', 'minimalist')"),
|
|
50
|
+
limit: z.number().optional().describe("Maximum number of cafes to return (default: 20)"),
|
|
51
|
+
}, async ({ query, hasWifi, hasOutlets, isPetFriendly, vibe, limit }) => {
|
|
52
|
+
try {
|
|
53
|
+
let data;
|
|
54
|
+
if (isActionGateway) {
|
|
55
|
+
data = await callGateway("/", "POST", {
|
|
56
|
+
action: "search",
|
|
57
|
+
query,
|
|
58
|
+
hasWifi,
|
|
59
|
+
hasOutlets,
|
|
60
|
+
isPetFriendly,
|
|
61
|
+
vibe,
|
|
62
|
+
limit: limit || 20,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const allCafes = await callGateway("/cafes", "GET");
|
|
67
|
+
let filtered = allCafes;
|
|
68
|
+
if (query) {
|
|
69
|
+
const q = query.toLowerCase();
|
|
70
|
+
filtered = filtered.filter((c) => (c.name && c.name.toLowerCase().includes(q)) ||
|
|
71
|
+
(c.address && c.address.toLowerCase().includes(q)));
|
|
72
|
+
}
|
|
73
|
+
if (hasWifi === true)
|
|
74
|
+
filtered = filtered.filter((c) => c.wifi);
|
|
75
|
+
if (hasOutlets === true)
|
|
76
|
+
filtered = filtered.filter((c) => c.outlets);
|
|
77
|
+
if (isPetFriendly === true)
|
|
78
|
+
filtered = filtered.filter((c) => c.pet_friendly);
|
|
79
|
+
if (vibe) {
|
|
80
|
+
const v = vibe.toLowerCase();
|
|
81
|
+
filtered = filtered.filter((c) => Array.isArray(c.vibe_tags) && c.vibe_tags.some((t) => t.toLowerCase().includes(v)));
|
|
82
|
+
}
|
|
83
|
+
const maxResults = limit || 20;
|
|
84
|
+
data = {
|
|
85
|
+
success: true,
|
|
86
|
+
count: Math.min(filtered.length, maxResults),
|
|
87
|
+
totalMatches: filtered.length,
|
|
88
|
+
cafes: filtered.slice(0, maxResults),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify(data, null, 2),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `Error searching cafes: ${error.message}`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Tool 2: Find Nearby Cafes
|
|
113
|
+
server.tool("find_nearby_cafes", "Find coffee shops nearest to a specific latitude and longitude coordinate within a given radius in kilometers.", {
|
|
114
|
+
latitude: z.number().describe("User latitude (e.g. 14.5547)"),
|
|
115
|
+
longitude: z.number().describe("User longitude (e.g. 121.0244)"),
|
|
116
|
+
radiusKm: z.number().optional().describe("Search radius in kilometers (default: 5.0)"),
|
|
117
|
+
limit: z.number().optional().describe("Maximum number of results to return (default: 15)"),
|
|
118
|
+
}, async ({ latitude, longitude, radiusKm = 5.0, limit = 15 }) => {
|
|
119
|
+
try {
|
|
120
|
+
let data;
|
|
121
|
+
if (isActionGateway) {
|
|
122
|
+
data = await callGateway("/", "POST", {
|
|
123
|
+
action: "nearby",
|
|
124
|
+
latitude,
|
|
125
|
+
longitude,
|
|
126
|
+
radiusKm,
|
|
127
|
+
limit,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const allCafes = await callGateway("/cafes", "GET");
|
|
132
|
+
// Haversine formula
|
|
133
|
+
const calculateDist = (lat1, lon1, lat2, lon2) => {
|
|
134
|
+
const R = 6371;
|
|
135
|
+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
136
|
+
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
137
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
138
|
+
Math.cos((lat1 * Math.PI) / 180) *
|
|
139
|
+
Math.cos((lat2 * Math.PI) / 180) *
|
|
140
|
+
Math.sin(dLon / 2) *
|
|
141
|
+
Math.sin(dLon / 2);
|
|
142
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
143
|
+
return Math.round(R * c * 100) / 100;
|
|
144
|
+
};
|
|
145
|
+
const nearby = allCafes
|
|
146
|
+
.filter((c) => c.latitude && c.longitude)
|
|
147
|
+
.map((c) => ({ ...c, distanceKm: calculateDist(latitude, longitude, c.latitude, c.longitude) }))
|
|
148
|
+
.filter((c) => c.distanceKm <= radiusKm)
|
|
149
|
+
.sort((a, b) => a.distanceKm - b.distanceKm)
|
|
150
|
+
.slice(0, limit);
|
|
151
|
+
data = {
|
|
152
|
+
success: true,
|
|
153
|
+
center: { latitude, longitude },
|
|
154
|
+
radiusKm,
|
|
155
|
+
count: nearby.length,
|
|
156
|
+
cafes: nearby,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: JSON.stringify(data, null, 2),
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: `Error finding nearby cafes: ${error.message}`,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// Tool 3: Get Cafe Details
|
|
181
|
+
server.tool("get_cafe_details", "Retrieve comprehensive details for a single cafe using its unique ID or URL slug.", {
|
|
182
|
+
idOrSlug: z.string().describe("The unique cafe ID (UUID) or URL slug (e.g. 'percent-arabica-manila-bgc-roastery')"),
|
|
183
|
+
}, async ({ idOrSlug }) => {
|
|
184
|
+
try {
|
|
185
|
+
let data;
|
|
186
|
+
if (isActionGateway) {
|
|
187
|
+
data = await callGateway("/", "POST", {
|
|
188
|
+
action: "get_cafe",
|
|
189
|
+
idOrSlug,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const allCafes = await callGateway("/cafes", "GET");
|
|
194
|
+
const cafe = allCafes.find((c) => c.id === idOrSlug || (c.slug && c.slug.toLowerCase() === idOrSlug.toLowerCase()));
|
|
195
|
+
if (!cafe) {
|
|
196
|
+
throw new Error(`Cafe with ID or slug '${idOrSlug}' was not found.`);
|
|
197
|
+
}
|
|
198
|
+
data = { success: true, cafe };
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "text",
|
|
204
|
+
text: JSON.stringify(data, null, 2),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `Error retrieving cafe: ${error.message}`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
// Tool 4: List Vibe Tags
|
|
222
|
+
server.tool("list_vibe_tags", "List all available mood, ambiance, and aesthetic tags that can be used to filter cafes.", {}, async () => {
|
|
223
|
+
try {
|
|
224
|
+
let data;
|
|
225
|
+
if (isActionGateway) {
|
|
226
|
+
data = await callGateway("/", "POST", {
|
|
227
|
+
action: "vibes",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const allCafes = await callGateway("/cafes", "GET");
|
|
232
|
+
const set = new Set();
|
|
233
|
+
allCafes.forEach((c) => {
|
|
234
|
+
if (Array.isArray(c.vibe_tags)) {
|
|
235
|
+
c.vibe_tags.forEach((v) => set.add(v));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
data = { success: true, vibes: Array.from(set).sort() };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: JSON.stringify(data, null, 2),
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: `Error fetching vibe tags: ${error.message}`,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
isError: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// Start the server using stdio transport
|
|
262
|
+
async function main() {
|
|
263
|
+
const transport = new StdioServerTransport();
|
|
264
|
+
await server.connect(transport);
|
|
265
|
+
console.error("☕ TaraKape MCP Server running on stdio");
|
|
266
|
+
}
|
|
267
|
+
main().catch((error) => {
|
|
268
|
+
console.error("Fatal error starting TaraKape MCP server:", error);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anotarakape/mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Model Context Protocol (MCP) Server for TaraKape Coffee Shops Directory",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-server": "./dist/index.js",
|
|
8
|
+
"tarakape-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsx src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
22
|
+
"zod": "^3.24.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.10.2",
|
|
26
|
+
"tsx": "^4.19.2",
|
|
27
|
+
"typescript": "^5.7.2"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["mcp", "model-context-protocol", "tarakape", "coffee", "manila", "cafes"],
|
|
30
|
+
"author": "TaraKape",
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|