SkateSpots/MCP_SERVER_SPEC.md
SkateSpots 6407310213 feat(relay): cut over community relay to self-hosted Newlay
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.
2026-10-12 12:38:13 +07:00

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