SkateSpots/MCP_SERVER_SPEC.md
shakespeare.diy 27e14994b5 Import files from skate-spots project
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 20:04:15 +07:00

17 KiB

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

{
  "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

Search for skate spots by location and type.

{
  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:

{
  "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.

{
  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.

{
  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.

{
  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.

{
  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:

{
  "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.

{
  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.

{
  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)

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)

import { NPool, NRelay1 } from '@nostrify/nostrify';

const RELAYS = [
  'wss://goskatespots.nostr1.com',
  '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)

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://goskatespots.nostr1.com',
  '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)

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

# 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)

# Run directly
npx skatespots-mcp

# Or with OpenClaw
openclaw skills add skatespots --path ./skatespots-mcp

Option 2: Docker

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)

# 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

// 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

# 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)

  • skatespots_search
  • skatespots_events
  • skatespots_coaches
  • skatespots_shops
  • 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

License

MIT