#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
// Strava API configuration
const STRAVA_API_BASE = "https://www.strava.com/api/v3";
interface StravaConfig {
accessToken: string;
refreshToken?: string;
clientId?: string;
clientSecret?: string;
}
class StravaServer {
private server: Server;
private config: StravaConfig;
constructor() {
this.server = new Server(
{
name: "strava-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Get configuration from environment variables
const accessToken = process.env.STRAVA_ACCESS_TOKEN;
if (!accessToken) {
throw new Error("STRAVA_ACCESS_TOKEN environment variable is required");
}
this.config = {
accessToken,
refreshToken: process.env.STRAVA_REFRESH_TOKEN,
clientId: process.env.STRAVA_CLIENT_ID,
clientSecret: process.env.STRAVA_CLIENT_SECRET,
};
this.setupHandlers();
}
private async stravaRequest(endpoint: string, options: any = {}) {
const url = `${STRAVA_API_BASE}${endpoint}`;
const headers = {
Authorization: `Bearer ${this.config.accessToken}`,
...options.headers,
};
const response = await fetch(url, { ...options, headers } as any);
if (!response.ok) {
const error = await response.text();
throw new Error(`Strava API error: ${response.status} - ${error}`);
}
return response.json();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "get_athlete_profile",
description: "Get the authenticated athlete's profile information including name, stats, and preferences",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_athlete_stats",
description: "Get detailed statistics for the authenticated athlete including totals and recent activity counts",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_activities",
description: "List the authenticated athlete's activities. Returns up to 30 activities by default.",
inputSchema: {
type: "object",
properties: {
before: {
type: "number",
description: "Unix timestamp to get activities before this time",
},
after: {
type: "number",
description: "Unix timestamp to get activities after this time",
},
page: {
type: "number",
description: "Page number (default: 1)",
},
per_page: {
type: "number",
description: "Number of items per page (default: 30, max: 200)",
},
},
},
},
{
name: "get_activity",
description: "Get detailed information about a specific activity by ID",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "The activity ID",
},
include_all_efforts: {
type: "boolean",
description: "Include all segment efforts (default: false)",
},
},
required: ["id"],
},
},
{
name: "get_segment",
description: "Get detailed information about a specific segment by ID",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "The segment ID",
},
},
required: ["id"],
},
},
{
name: "list_starred_segments",
description: "List segments starred by the authenticated athlete",
inputSchema: {
type: "object",
properties: {
page: {
type: "number",
description: "Page number (default: 1)",
},
per_page: {
type: "number",
description: "Number of items per page (default: 30, max: 200)",
},
},
},
},
{
name: "explore_segments",
description: "Explore segments in a given area. Returns the top 10 segments matching the search criteria.",
inputSchema: {
type: "object",
properties: {
bounds: {
type: "string",
description: "Comma-separated string of coordinates: sw_lat,sw_lng,ne_lat,ne_lng (e.g., '37.7,-122.5,37.8,-122.4')",
},
activity_type: {
type: "string",
description: "Activity type: 'running' or 'riding'",
enum: ["running", "riding"],
},
min_cat: {
type: "number",
description: "Minimum climb category (0-5)",
},
max_cat: {
type: "number",
description: "Maximum climb category (0-5)",
},
},
required: ["bounds"],
},
},
{
name: "get_segment_efforts",
description: "Get efforts on a specific segment",
inputSchema: {
type: "object",
properties: {
segment_id: {
type: "string",
description: "The segment ID",
},
start_date_local: {
type: "string",
description: "ISO 8601 formatted date time (e.g., '2024-01-01T00:00:00Z')",
},
end_date_local: {
type: "string",
description: "ISO 8601 formatted date time (e.g., '2024-12-31T23:59:59Z')",
},
per_page: {
type: "number",
description: "Number of items per page (default: 30, max: 200)",
},
},
required: ["segment_id"],
},
},
];
return { tools };
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const toolArgs: any = args || {};
try {
switch (name) {
case "get_athlete_profile": {
const data = await this.stravaRequest("/athlete");
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "get_athlete_stats": {
const athlete: any = await this.stravaRequest("/athlete");
const stats = await this.stravaRequest(`/athletes/${athlete.id}/stats`);
return {
content: [
{
type: "text",
text: JSON.stringify(stats, null, 2),
},
],
};
}
case "list_activities": {
const params = new URLSearchParams();
if (toolArgs.before) params.append("before", String(toolArgs.before));
if (toolArgs.after) params.append("after", String(toolArgs.after));
if (toolArgs.page) params.append("page", String(toolArgs.page));
if (toolArgs.per_page) params.append("per_page", String(toolArgs.per_page));
const queryString = params.toString();
const endpoint = `/athlete/activities${queryString ? `?${queryString}` : ""}`;
const data = await this.stravaRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "get_activity": {
const params = new URLSearchParams();
if (toolArgs.include_all_efforts) {
params.append("include_all_efforts", "true");
}
const queryString = params.toString();
const endpoint = `/activities/${toolArgs.id}${queryString ? `?${queryString}` : ""}`;
const data = await this.stravaRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "get_segment": {
const data = await this.stravaRequest(`/segments/${toolArgs.id}`);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "list_starred_segments": {
const params = new URLSearchParams();
if (toolArgs.page) params.append("page", String(toolArgs.page));
if (toolArgs.per_page) params.append("per_page", String(toolArgs.per_page));
const queryString = params.toString();
const endpoint = `/segments/starred${queryString ? `?${queryString}` : ""}`;
const data = await this.stravaRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "explore_segments": {
const params = new URLSearchParams();
params.append("bounds", toolArgs.bounds);
if (toolArgs.activity_type) params.append("activity_type", toolArgs.activity_type);
if (toolArgs.min_cat !== undefined) params.append("min_cat", String(toolArgs.min_cat));
if (toolArgs.max_cat !== undefined) params.append("max_cat", String(toolArgs.max_cat));
const data = await this.stravaRequest(`/segments/explore?${params.toString()}`);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
case "get_segment_efforts": {
const params = new URLSearchParams();
params.append("segment_id", toolArgs.segment_id);
if (toolArgs.start_date_local) params.append("start_date_local", toolArgs.start_date_local);
if (toolArgs.end_date_local) params.append("end_date_local", toolArgs.end_date_local);
if (toolArgs.per_page) params.append("per_page", String(toolArgs.per_page));
const data = await this.stravaRequest(`/segment_efforts?${params.toString()}`);
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Strava MCP Server running on stdio");
}
}
const server = new StravaServer();
server.run().catch(console.error);