@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.
Files changed (3) hide show
  1. package/README.md +106 -2
  2. package/dist/index.js +270 -0
  3. package/package.json +30 -4
package/README.md CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
1
+ # ☕ TaraKape MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }