Skip to content

Commit e972766

Browse files
authored
Merge pull request #468 from HelpCode-ai/keysersoft/playtomic-api-migration
fix(connectors): migrate Playtomic to api.app.playtomic.io + require account (v0.4.1)
2 parents 37bbbd8 + 24985a3 commit e972766

9 files changed

Lines changed: 77 additions & 54 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anythingmcp",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "Self-hosted MCP gateway for REST, SOAP/WSDL, GraphQL and SQL — turn any API into MCP tools for Claude, ChatGPT, Gemini, Copilot and Cursor. 30+ pre-built adapters, on-prem audit log, OAuth2/RBAC. Open source (AGPL-3.0).",
55
"private": true,
66
"license": "AGPL-3.0-only",

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@anythingmcp/backend",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "AnythingMCP — NestJS Backend + Dynamic MCP Server",
55
"private": true,
66
"license": "AGPL-3.0-only",

packages/backend/src/adapters/intl/playtomic-public.json

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
11
{
22
"slug": "playtomic-public",
3-
"name": "Playtomic (Public)",
4-
"description": "Find padel, tennis, and futsal courts worldwide — read-only, no account needed. Search clubs near you, check court availability and pricing, browse Playtomic's tenant catalog. 4 tools, zero setup.",
5-
"instructions": "This connector wraps Playtomic's **public** read endpoints — no login, no API key, no setup. It's the fastest way to give an AI agent the ability to find padel/tennis courts and check availability.\n\n**What you can do without an account**:\n- Search clubs (tenants) by geo coordinate + radius, optionally filtered by sport.\n- Fetch a single club's full details (resources/courts, opening info, address, timezone, images).\n- Read free time slots for a given day across one or more clubs, with prices.\n- Read Playtomic's sport configuration (allowed durations, resource types, sport list).\n\n**What requires the full Playtomic connector instead**:\n- Your own profile, level, match history, social graph.\n- Open matches you can join.\n- Member/club-specific discounted pricing (this connector returns generic pricing).\n- Booking a court (write operations are not exposed — Playtomic has no official public booking API).\n\n**Pricing notes**:\n- `price` is a string like `\"48 EUR\"` — parse amount + currency separately.\n- Same `start_time` may appear multiple times with different `duration` values (60/90/120 min). These are alternative bookings of the same slot, not separate free slots.\n- `duration` is in **minutes**.\n\n**Time handling**:\n- `local_start_min` / `local_start_max` are naive ISO-8601 in the tenant's local timezone — find the timezone in `tenant.address.timezone` (e.g. `Europe/Berlin`).\n- For a full-day window use `2026-05-23T00:00:00` → `2026-05-23T23:59:59`.\n\n**Joining availability back to courts**: every availability row has a `resource_id`; map it back to `tenant.resources[].name` from `get_tenant` to know which court (indoor/outdoor, single/double, etc.).\n\n**Rate limits**: Playtomic has no published public-API SLA. Be gentle — under 1 req/s is safe. The connector identifies itself honestly as AnythingMCP (no UA spoofing).\n\n**Stability**: this is an undocumented/internal API reverse-engineered from the iOS app. Endpoints can change without notice. If a tool breaks, check the docs URL for updates.\n\n**Suggested starter prompts**:\n- \"Find padel clubs within 30 km of Milan that are open today.\"\n- \"What time slots are free on Saturday afternoon at FREIBURG PADEL?\"\n- \"Compare prices across all padel clubs in Madrid for Sunday morning, 90-minute sessions.\"\n- \"Which courts in Barcelona offer indoor crystal padel?\"",
3+
"name": "Playtomic (Lite)",
4+
"description": "Find padel, tennis, and futsal courts worldwide — read-only. Search clubs near you, check court availability and pricing, browse Playtomic's tenant catalog. 4 read tools; needs a free Playtomic account (email + password), no API key.",
5+
"instructions": "This is the **lite, read-only** Playtomic connector — the fastest way to give an AI agent the ability to find padel/tennis courts and check availability, without the 13-tool full connector's personal-data scope.\n\n**Authentication (handled automatically)**: Playtomic closed anonymous access to its API in mid-2025, so even read-only club search now needs an authenticated session. Provide a **free** Playtomic account (`PLAYTOMIC_EMAIL` + `PLAYTOMIC_PASSWORD`); the AnythingMCP `LOGIN_TOKEN` engine logs in, attaches `Authorization: Bearer <jwt>` to every call, caches the token encrypted, and re-logs in automatically ~60 s before expiry and on any `401`. Your password is stored encrypted and only sent over TLS to Playtomic's login endpoint. No club membership is required — any registered Playtomic user works.\n\n**What you can do**:\n- Search clubs (tenants) by geo coordinate + radius, optionally filtered by sport.\n- Fetch a single club's full details (resources/courts, opening info, address, timezone, images).\n- Read free time slots for a given day across one or more clubs, with prices.\n- Read Playtomic's sport configuration (allowed durations, resource types, sport list).\n\n**What requires the full Playtomic connector instead**:\n- Your own profile, level, match history, social graph.\n- Open matches you can join.\n- Member/club-specific discounted pricing (this connector returns generic pricing).\n- Booking a court (write operations are not exposed — Playtomic has no official public booking API).\n\n**Pricing notes**:\n- `price` is a string like `\"48 EUR\"` — parse amount + currency separately.\n- Same `start_time` may appear multiple times with different `duration` values (60/90/120 min). These are alternative bookings of the same slot, not separate free slots.\n- `duration` is in **minutes**.\n\n**Time handling**:\n- `local_start_min` / `local_start_max` are naive ISO-8601 in the tenant's local timezone — find the timezone in `tenant.address.timezone` (e.g. `Europe/Berlin`).\n- For a full-day window use `2026-05-23T00:00:00` → `2026-05-23T23:59:59`.\n\n**Joining availability back to courts**: every availability row has a `resource_id`; map it back to `tenant.resources[].name` from `get_tenant` to know which court (indoor/outdoor, single/double, etc.).\n\n**Rate limits**: Playtomic has no published public-API SLA. Be gentle — under 1 req/s is safe. The connector identifies itself honestly as AnythingMCP (no UA spoofing).\n\n**Stability**: this is an undocumented/internal API reverse-engineered from the iOS app. Endpoints can change without notice. If a tool breaks, check the docs URL for updates.\n\n**Suggested starter prompts**:\n- \"Find padel clubs within 30 km of Milan that are open today.\"\n- \"What time slots are free on Saturday afternoon at FREIBURG PADEL?\"\n- \"Compare prices across all padel clubs in Madrid for Sunday morning, 90-minute sessions.\"\n- \"Which courts in Barcelona offer indoor crystal padel?\"",
66
"region": "intl",
77
"category": "sports",
88
"icon": "playtomic",
99
"docsUrl": "https://playtomic.io",
1010
"featured": true,
1111
"priority": 80,
12-
"requiredEnvVars": [],
12+
"requiredEnvVars": [
13+
"PLAYTOMIC_EMAIL",
14+
"PLAYTOMIC_PASSWORD"
15+
],
1316
"connector": {
14-
"name": "Playtomic Public API",
17+
"name": "Playtomic Lite",
1518
"type": "REST",
16-
"baseUrl": "https://app.playtomic.io",
17-
"authType": "NONE",
18-
"authConfig": {}
19+
"baseUrl": "https://api.app.playtomic.io",
20+
"authType": "LOGIN_TOKEN",
21+
"authConfig": {
22+
"loginUrl": "https://api.app.playtomic.io/v3/auth/login",
23+
"loginMethod": "POST",
24+
"loginBody": {
25+
"requested_user_roles": [
26+
"ROLE_CUSTOMER"
27+
],
28+
"email": "${username}",
29+
"password": "${password}"
30+
},
31+
"username": "{{PLAYTOMIC_EMAIL}}",
32+
"password": "{{PLAYTOMIC_PASSWORD}}",
33+
"tokenJsonPath": "access_token",
34+
"expiryJsonPath": "access_token_expiration",
35+
"expiryFormat": "iso8601",
36+
"refreshOn401": true,
37+
"proactiveRefreshSeconds": 60,
38+
"headerName": "Authorization",
39+
"headerTemplate": "Bearer ${token}"
40+
}
1941
},
2042
"tools": [
2143
{
@@ -62,7 +84,7 @@
6284
},
6385
"endpointMapping": {
6486
"method": "GET",
65-
"path": "/api/v1/tenants",
87+
"path": "/v1/tenants",
6688
"queryParams": {
6789
"coordinate": "$coordinate",
6890
"radius": "$radius",
@@ -91,7 +113,7 @@
91113
},
92114
"endpointMapping": {
93115
"method": "GET",
94-
"path": "/api/v1/tenants/{tenant_id}"
116+
"path": "/v1/tenants/{tenant_id}"
95117
}
96118
},
97119
{
@@ -132,7 +154,7 @@
132154
},
133155
"endpointMapping": {
134156
"method": "GET",
135-
"path": "/api/v1/availability",
157+
"path": "/v1/availability",
136158
"queryParams": {
137159
"sport_id": "$sport_id",
138160
"tenant_id": "$tenant_id",
@@ -151,7 +173,7 @@
151173
},
152174
"endpointMapping": {
153175
"method": "GET",
154-
"path": "/api/v2/configuration"
176+
"path": "/v2/configuration"
155177
}
156178
}
157179
]

packages/backend/src/adapters/intl/playtomic-public.live.spec.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ const a = adapter as unknown as {
1212
};
1313

1414
describe('playtomic-public adapter — static spec conformance', () => {
15-
it('is the read-only NONE-auth variant in the sports category', () => {
15+
it('is the read-only LOGIN_TOKEN lite variant in the sports category', () => {
1616
expect(a.slug).toBe('playtomic-public');
1717
expect(a.category).toBe('sports');
18-
expect(a.connector.baseUrl).toBe('https://app.playtomic.io');
19-
expect(a.connector.authType).toBe('NONE');
20-
expect(a.requiredEnvVars).toEqual([]);
18+
expect(a.connector.baseUrl).toBe('https://api.app.playtomic.io');
19+
expect(a.connector.authType).toBe('LOGIN_TOKEN');
20+
expect(a.requiredEnvVars).toEqual(['PLAYTOMIC_EMAIL', 'PLAYTOMIC_PASSWORD']);
21+
});
22+
23+
it('logs in via /v3/auth/login and reads access_token like the full connector', () => {
24+
const auth = a.connector.authConfig as Record<string, unknown>;
25+
expect(auth.loginUrl).toBe('https://api.app.playtomic.io/v3/auth/login');
26+
expect(auth.tokenJsonPath).toBe('access_token');
27+
expect(auth.expiryJsonPath).toBe('access_token_expiration');
28+
const body = auth.loginBody as Record<string, unknown>;
29+
expect(body.requested_user_roles).toEqual(['ROLE_CUSTOMER']);
2130
});
2231

2332
it('exposes exactly the 4 public read tools', () => {
@@ -32,14 +41,14 @@ describe('playtomic-public adapter — static spec conformance', () => {
3241
it('search_tenants forces playtomic_status=ACTIVE and accepts geo coordinate', () => {
3342
const t = a.tools.find((x) => x.name === 'playtomic_search_tenants')!;
3443
expect(t.endpointMapping.method).toBe('GET');
35-
expect(t.endpointMapping.path).toBe('/api/v1/tenants');
44+
expect(t.endpointMapping.path).toBe('/v1/tenants');
3645
expect(t.endpointMapping.queryParams?.coordinate).toBe('$coordinate');
3746
expect(t.endpointMapping.queryParams?.playtomic_status).toBe('ACTIVE');
3847
});
3948

40-
it('availability uses the documented /api/v1/availability path and naive ISO bounds', () => {
49+
it('availability uses the documented /v1/availability path and naive ISO bounds', () => {
4150
const t = a.tools.find((x) => x.name === 'playtomic_get_availability')!;
42-
expect(t.endpointMapping.path).toBe('/api/v1/availability');
51+
expect(t.endpointMapping.path).toBe('/v1/availability');
4352
expect(t.endpointMapping.queryParams?.local_start_min).toBe('$local_start_min');
4453
expect(t.endpointMapping.queryParams?.local_start_max).toBe('$local_start_max');
4554
});

0 commit comments

Comments
 (0)