Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
687 lines
17 KiB
Markdown
687 lines
17 KiB
Markdown
# SkateSpots MCP Server Specification
|
|
|
|
A Model Context Protocol (MCP) server that exposes SkateSpots data to AI agents via ContextVM (MCP over Nostr).
|
|
|
|
## Overview
|
|
|
|
This document specifies a **separate project** that provides AI agents (like OpenClaw) access to SkateSpots data. The MCP server:
|
|
|
|
1. Queries Nostr relays for spots, events, coaches, and shops
|
|
2. Exposes tools via the MCP protocol
|
|
3. Optionally integrates with ContextVM for Nostr-native transport
|
|
4. Optionally supports CEP-8 payments for premium features
|
|
|
|
**Important**: This is a standalone service. It does NOT modify the SkateSpots web app.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ AI AGENT CLIENTS │
|
|
│ │
|
|
│ OpenClaw Claude Desktop Other MCP Clients Custom Agents │
|
|
└───────────────────────────────┬─────────────────────────────────────────┘
|
|
│
|
|
┌───────────▼───────────┐
|
|
│ TRANSPORT LAYER │
|
|
│ │
|
|
│ Option A: stdio │ ← Local development
|
|
│ Option B: SSE/HTTP │ ← Traditional hosting
|
|
│ Option C: ContextVM │ ← Nostr-native (recommended)
|
|
└───────────┬───────────┘
|
|
│
|
|
┌───────────▼───────────┐
|
|
│ SKATESPOTS MCP │
|
|
│ SERVER │
|
|
│ │
|
|
│ Tools: │
|
|
│ • skatespots_search │
|
|
│ • skatespots_events │
|
|
│ • skatespots_coaches │
|
|
│ • skatespots_shops │
|
|
│ • skatespots_weather │
|
|
└───────────┬───────────┘
|
|
│
|
|
┌───────────▼───────────┐
|
|
│ NOSTR RELAYS │
|
|
│ │
|
|
│ goskatespots.nostr1 │
|
|
│ wotr.relatr.xyz │
|
|
└───────────────────────┘
|
|
```
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
skatespots-mcp/
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── README.md
|
|
├── SKILL.md # OpenClaw skill definition
|
|
├── src/
|
|
│ ├── index.ts # Entry point
|
|
│ ├── server.ts # MCP server setup
|
|
│ ├── tools/
|
|
│ │ ├── index.ts # Tool registry
|
|
│ │ ├── search.ts # skatespots_search
|
|
│ │ ├── events.ts # skatespots_events
|
|
│ │ ├── coaches.ts # skatespots_coaches
|
|
│ │ ├── shops.ts # skatespots_shops
|
|
│ │ ├── weather.ts # skatespots_weather
|
|
│ │ └── premium/
|
|
│ │ ├── recommend.ts # AI recommendations (paid)
|
|
│ │ └── route.ts # Route planner (paid)
|
|
│ ├── nostr/
|
|
│ │ ├── client.ts # Nostr query client
|
|
│ │ ├── relays.ts # Relay configuration
|
|
│ │ └── parsers.ts # Event parsers (from SkateSpots)
|
|
│ ├── contextvm/
|
|
│ │ ├── gateway.ts # ContextVM gateway setup
|
|
│ │ └── payments.ts # CEP-8 payment configuration
|
|
│ └── utils/
|
|
│ ├── geo.ts # Geohash utilities
|
|
│ └── weather.ts # Weather API client
|
|
└── tests/
|
|
└── tools.test.ts
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
"@contextvm/sdk": "^0.1.0",
|
|
"@nostrify/nostrify": "^0.32.0",
|
|
"ngeohash": "^0.6.3",
|
|
"zod": "^3.23.0"
|
|
},
|
|
"devDependencies": {
|
|
"typescript": "^5.0.0",
|
|
"tsx": "^4.0.0",
|
|
"vitest": "^2.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Tool Definitions
|
|
|
|
### Free Tools
|
|
|
|
#### 1. `skatespots_search`
|
|
|
|
Search for skate spots by location and type.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_search',
|
|
description: 'Search for skate spots (parks, street spots, DIY spots, bowls) by location and type',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'Search query (location name, city, or keywords)'
|
|
},
|
|
type: {
|
|
type: 'string',
|
|
enum: ['street', 'park', 'diy', 'bowl', 'transition', 'plaza', 'stairs', 'rail', 'ledge', 'gap'],
|
|
description: 'Filter by spot type'
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
default: 10,
|
|
description: 'Maximum number of results'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"spots": [
|
|
{
|
|
"title": "Hollywood High School",
|
|
"location": "Los Angeles, CA",
|
|
"types": ["street", "stairs", "ledge"],
|
|
"description": "Classic LA spot with a perfect 10-stair...",
|
|
"image": "https://...",
|
|
"coordinates": { "lat": 34.1017, "lng": -118.3384 }
|
|
}
|
|
],
|
|
"total": 1
|
|
}
|
|
```
|
|
|
|
#### 2. `skatespots_events`
|
|
|
|
Find skateboarding events.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_events',
|
|
description: 'Find skateboarding events, contests, demos, video premieres, and concerts',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
location: {
|
|
type: 'string',
|
|
description: 'City or area to search'
|
|
},
|
|
type: {
|
|
type: 'string',
|
|
enum: ['contest', 'demo', 'session', 'workshop', 'premiere', 'tradeshow', 'gameskate', 'concert', 'jam', 'festival', 'popup'],
|
|
description: 'Filter by event type'
|
|
},
|
|
timeframe: {
|
|
type: 'string',
|
|
enum: ['today', 'this_week', 'this_weekend', 'this_month', 'upcoming'],
|
|
default: 'upcoming',
|
|
description: 'Time range for events'
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
default: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. `skatespots_coaches`
|
|
|
|
Find skateboarding coaches and instructors.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_coaches',
|
|
description: 'Find skateboarding coaches and instructors by location and skills',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
location: {
|
|
type: 'string',
|
|
description: 'City or area'
|
|
},
|
|
skill: {
|
|
type: 'string',
|
|
enum: ['basics', 'ollies', 'kickflips', 'grinds', 'manuals', 'inverts', 'vert', 'bowl', 'street', 'freestyle'],
|
|
description: 'Skill to learn'
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
default: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. `skatespots_shops`
|
|
|
|
Find local skate shops.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_shops',
|
|
description: 'Find local skateboard shops',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
location: {
|
|
type: 'string',
|
|
description: 'City or area'
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
default: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. `skatespots_weather`
|
|
|
|
Get weather conditions at a spot.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_weather',
|
|
description: 'Get current weather and skating conditions at a spot',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
spot_name: {
|
|
type: 'string',
|
|
description: 'Name of the skate spot'
|
|
},
|
|
coordinates: {
|
|
type: 'object',
|
|
properties: {
|
|
lat: { type: 'number' },
|
|
lng: { type: 'number' }
|
|
},
|
|
description: 'Or provide coordinates directly'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"spot": "Hollywood High School",
|
|
"weather": {
|
|
"temperature": 72,
|
|
"unit": "F",
|
|
"condition": "Sunny",
|
|
"humidity": 45,
|
|
"wind_speed": 5,
|
|
"precipitation_chance": 0
|
|
},
|
|
"skateability": {
|
|
"score": 95,
|
|
"status": "Perfect",
|
|
"notes": "Dry conditions, light wind - ideal for skating"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Premium Tools (CEP-8 Payments)
|
|
|
|
#### 6. `skatespots_recommend` (50 sats)
|
|
|
|
AI-powered spot recommendations.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_recommend',
|
|
description: 'Get AI-powered spot recommendations based on preferences or similar spots',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
like: {
|
|
type: 'string',
|
|
description: 'Find spots similar to this one (e.g., "MACBA", "Love Park")'
|
|
},
|
|
style: {
|
|
type: 'string',
|
|
enum: ['street', 'transition', 'technical', 'big gaps', 'mellow'],
|
|
description: 'Skating style preference'
|
|
},
|
|
skill_level: {
|
|
type: 'string',
|
|
enum: ['beginner', 'intermediate', 'advanced', 'pro'],
|
|
description: 'Skill level'
|
|
},
|
|
location: {
|
|
type: 'string',
|
|
description: 'Area to search'
|
|
}
|
|
},
|
|
required: ['location']
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 7. `skatespots_route` (25 sats)
|
|
|
|
Multi-spot session planner.
|
|
|
|
```typescript
|
|
{
|
|
name: 'skatespots_route',
|
|
description: 'Plan an optimized route for a multi-spot skate session',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
start_location: {
|
|
type: 'string',
|
|
description: 'Starting point'
|
|
},
|
|
duration_hours: {
|
|
type: 'number',
|
|
default: 3,
|
|
description: 'How long is your session'
|
|
},
|
|
spot_types: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Types of spots to include'
|
|
},
|
|
max_spots: {
|
|
type: 'number',
|
|
default: 5
|
|
}
|
|
},
|
|
required: ['start_location']
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementation
|
|
|
|
### Core Server (`src/server.ts`)
|
|
|
|
```typescript
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { tools } from './tools/index.js';
|
|
|
|
const server = new Server(
|
|
{
|
|
name: 'skatespots-mcp',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
// Register tools
|
|
server.setRequestHandler('tools/list', async () => ({
|
|
tools: Object.values(tools).map(t => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
inputSchema: t.inputSchema,
|
|
})),
|
|
}));
|
|
|
|
server.setRequestHandler('tools/call', async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
const tool = tools[name];
|
|
|
|
if (!tool) {
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
|
|
const result = await tool.handler(args);
|
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
});
|
|
|
|
// Start server
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
```
|
|
|
|
### Nostr Client (`src/nostr/client.ts`)
|
|
|
|
```typescript
|
|
import { NPool, NRelay1 } from '@nostrify/nostrify';
|
|
|
|
const RELAYS = [
|
|
'wss://relay.skate-spots.org',
|
|
'wss://wotr.relatr.xyz',
|
|
];
|
|
|
|
export const nostrPool = new NPool({
|
|
open(url: string) {
|
|
return new NRelay1(url);
|
|
},
|
|
reqRouter(filters) {
|
|
const routes = new Map();
|
|
for (const relay of RELAYS) {
|
|
routes.set(relay, filters);
|
|
}
|
|
return routes;
|
|
},
|
|
eventRouter() {
|
|
return RELAYS;
|
|
},
|
|
});
|
|
|
|
export async function querySpots(filter: object) {
|
|
const events = await nostrPool.query([
|
|
{
|
|
kinds: [30818],
|
|
'#t': ['skatespot'],
|
|
...filter,
|
|
},
|
|
]);
|
|
return events;
|
|
}
|
|
|
|
export async function queryEvents(filter: object) {
|
|
const events = await nostrPool.query([
|
|
{
|
|
kinds: [31923],
|
|
'#t': ['skateboarding'],
|
|
...filter,
|
|
},
|
|
]);
|
|
return events;
|
|
}
|
|
```
|
|
|
|
### Event Parsers (`src/nostr/parsers.ts`)
|
|
|
|
Copy directly from SkateSpots web app:
|
|
- `parseSkateSpot()`
|
|
- `parseSkateEvent()`
|
|
- `parseSkateCoach()`
|
|
- `parseSkateShop()`
|
|
|
|
These are pure functions with no UI dependencies.
|
|
|
|
## ContextVM Integration
|
|
|
|
### Gateway Setup (`src/contextvm/gateway.ts`)
|
|
|
|
```typescript
|
|
import { NostrMCPGateway } from '@contextvm/sdk';
|
|
import { PrivateKeySigner } from '@contextvm/sdk';
|
|
import { SimpleRelayPool } from '@contextvm/sdk';
|
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
|
|
const signer = new PrivateKeySigner(process.env.NOSTR_PRIVATE_KEY!);
|
|
const relayPool = new SimpleRelayPool([
|
|
'wss://relay.skate-spots.org',
|
|
'wss://wotr.relatr.xyz',
|
|
]);
|
|
|
|
const gateway = new NostrMCPGateway({
|
|
mcpClientTransport: new StdioClientTransport({
|
|
command: 'node',
|
|
args: ['dist/index.js'],
|
|
}),
|
|
nostrTransportOptions: {
|
|
signer,
|
|
relayHandler: relayPool,
|
|
isPublicServer: true, // CEP-6: Announce on Nostr
|
|
},
|
|
});
|
|
|
|
await gateway.start();
|
|
console.log('SkateSpots MCP server available on Nostr');
|
|
```
|
|
|
|
### CEP-8 Payments (`src/contextvm/payments.ts`)
|
|
|
|
```typescript
|
|
import { withServerPayments } from '@contextvm/sdk/payments';
|
|
import { LnBolt11NwcPaymentProcessor } from '@contextvm/sdk/payments';
|
|
|
|
const paymentProcessor = new LnBolt11NwcPaymentProcessor({
|
|
nwcUri: process.env.NWC_URI!, // Nostr Wallet Connect URI
|
|
});
|
|
|
|
const pricedCapabilities = [
|
|
{
|
|
method: 'tools/call',
|
|
name: 'skatespots_recommend',
|
|
amount: 50,
|
|
currencyUnit: 'sats',
|
|
},
|
|
{
|
|
method: 'tools/call',
|
|
name: 'skatespots_route',
|
|
amount: 25,
|
|
currencyUnit: 'sats',
|
|
},
|
|
];
|
|
|
|
export function wrapWithPayments(transport) {
|
|
return withServerPayments(transport, {
|
|
processors: [paymentProcessor],
|
|
pricedCapabilities,
|
|
});
|
|
}
|
|
```
|
|
|
|
## OpenClaw Skill Definition
|
|
|
|
### `SKILL.md`
|
|
|
|
```markdown
|
|
# SkateSpots
|
|
|
|
Find skate spots, events, coaches, and shops worldwide using Nostr.
|
|
|
|
## Tools
|
|
|
|
### skatespots_search
|
|
Search for skate spots by location and type.
|
|
- `query`: Location or search terms
|
|
- `type`: street, park, diy, bowl, etc.
|
|
|
|
### skatespots_events
|
|
Find skateboarding events.
|
|
- `location`: City or area
|
|
- `type`: contest, demo, premiere, concert, jam, etc.
|
|
- `timeframe`: today, this_week, this_weekend, upcoming
|
|
|
|
### skatespots_coaches
|
|
Find skateboarding coaches.
|
|
- `location`: City or area
|
|
- `skill`: basics, kickflips, grinds, bowl, etc.
|
|
|
|
### skatespots_shops
|
|
Find local skate shops.
|
|
- `location`: City or area
|
|
|
|
### skatespots_weather
|
|
Get skating conditions at a spot.
|
|
- `spot_name`: Name of the spot
|
|
|
|
## Examples
|
|
|
|
"Find me a bowl near Los Angeles"
|
|
"Any skate events with live music this weekend?"
|
|
"What's the weather like at Hollywood High?"
|
|
"Find a kickflip coach in NYC"
|
|
```
|
|
|
|
## Deployment Options
|
|
|
|
### Option 1: Local Development (stdio)
|
|
|
|
```bash
|
|
# Run directly
|
|
npx skatespots-mcp
|
|
|
|
# Or with OpenClaw
|
|
openclaw skills add skatespots --path ./skatespots-mcp
|
|
```
|
|
|
|
### Option 2: Docker
|
|
|
|
```dockerfile
|
|
FROM node:22-alpine
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm ci --production
|
|
COPY dist ./dist
|
|
CMD ["node", "dist/index.js"]
|
|
```
|
|
|
|
### Option 3: ContextVM Gateway (Nostr-native)
|
|
|
|
```bash
|
|
# Start gateway that exposes MCP server on Nostr
|
|
NOSTR_PRIVATE_KEY=nsec1... node dist/gateway.js
|
|
```
|
|
|
|
Any ContextVM-compatible client can now discover and use the server via Nostr.
|
|
|
|
## Testing
|
|
|
|
```typescript
|
|
// tests/tools.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { searchSpots } from '../src/tools/search.js';
|
|
|
|
describe('skatespots_search', () => {
|
|
it('finds spots by location', async () => {
|
|
const result = await searchSpots({ query: 'Los Angeles', limit: 5 });
|
|
expect(result.spots).toBeDefined();
|
|
expect(result.spots.length).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('filters by type', async () => {
|
|
const result = await searchSpots({ query: 'LA', type: 'bowl' });
|
|
result.spots.forEach(spot => {
|
|
expect(spot.types).toContain('bowl');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
```bash
|
|
# Required for ContextVM/Nostr transport
|
|
NOSTR_PRIVATE_KEY=nsec1...
|
|
|
|
# Required for CEP-8 payments
|
|
NWC_URI=nostr+walletconnect://...
|
|
|
|
# Optional: Weather API
|
|
WEATHER_API_KEY=...
|
|
|
|
# Optional: Custom relays
|
|
NOSTR_RELAYS=wss://relay1.com,wss://relay2.com
|
|
```
|
|
|
|
## Roadmap
|
|
|
|
### Phase 1: Core Tools (MVP)
|
|
- [x] skatespots_search
|
|
- [x] skatespots_events
|
|
- [x] skatespots_coaches
|
|
- [x] skatespots_shops
|
|
- [x] skatespots_weather
|
|
|
|
### Phase 2: ContextVM Integration
|
|
- [ ] Nostr transport via ContextVM Gateway
|
|
- [ ] CEP-6 public server announcement
|
|
- [ ] Server discovery on Nostr
|
|
|
|
### Phase 3: Premium Features
|
|
- [ ] CEP-8 payment integration
|
|
- [ ] skatespots_recommend (AI-powered)
|
|
- [ ] skatespots_route (session planner)
|
|
- [ ] skatespots_crowd (crowd predictions)
|
|
|
|
### Phase 4: Advanced
|
|
- [ ] Real-time event notifications
|
|
- [ ] Spot check-ins
|
|
- [ ] Session coordination
|
|
- [ ] AI video analysis
|
|
|
|
## Resources
|
|
|
|
- [MCP SDK Documentation](https://modelcontextprotocol.io/docs)
|
|
- [ContextVM Documentation](https://docs.contextvm.org)
|
|
- [OpenClaw Skills](https://clawhub.com)
|
|
- [SkateSpots Web App](https://skate-spots.org)
|
|
- [Nostr Protocol](https://nostr.com)
|
|
|
|
## License
|
|
|
|
MIT
|