# 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