get-activity-photos
Retrieve photos from a Strava activity to access images, URLs, and metadata for display or download.
Instructions
Retrieves photos associated with a specific Strava activity.
Use Cases:
Fetch all photos uploaded to an activity
Get photo URLs for display or download
Access photo metadata including location and timestamps
Parameters:
id (required): The unique identifier of the Strava activity.
size (optional): Size of photos to return in pixels (e.g., 100, 600, 2048). If not specified, returns all available sizes.
Output Format: Returns both a human-readable summary and complete JSON data for each photo, including:
A text summary with photo count and URLs
Raw photo data containing all fields from the Strava API:
Photo ID and unique identifier
URLs for different sizes
Source (1 = Strava, 2 = Instagram)
Timestamps (uploaded_at, created_at)
Location coordinates if available
Caption if provided
Notes:
Requires activity:read scope for public/followers activities, activity:read_all for private activities
Photos may come from Strava uploads or linked Instagram posts
Returns empty array if activity has no photos
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The identifier of the activity to fetch photos for. | |
| size | No | Optional photo size in pixels (e.g., 100, 600, 2048). |
Implementation Reference
- src/tools/getActivityPhotos.ts:46-143 (handler)The main handler function (execute) for the get-activity-photos tool. It processes input, fetches photos from Strava API using the client helper, generates a formatted summary with photo details, and returns both human-readable text and raw JSON data.execute: async ({ id, size }: GetActivityPhotosInput) => { const token = process.env.STRAVA_ACCESS_TOKEN; if (!token) { console.error("Missing STRAVA_ACCESS_TOKEN environment variable."); return { content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }], isError: true }; } try { // Convert id to number if it's a string const activityId = typeof id === 'string' ? parseInt(id, 10) : id; if (isNaN(activityId)) { return { content: [{ type: "text" as const, text: `Invalid activity ID: ${id}` }], isError: true }; } console.error(`Fetching photos for activity ID: ${activityId}...`); const photos = await getActivityPhotosClient(token, activityId, size); if (!photos || photos.length === 0) { return { content: [{ type: "text" as const, text: `No photos found for activity ID: ${activityId}` }] }; } // Generate human-readable summary const photoSummaries = photos.map((photo, index) => { const details = [ `Photo ${index + 1}${photo.id ? ` (ID: ${photo.id})` : ''}${photo.unique_id ? ` [${photo.unique_id}]` : ''}`, ]; // Add source info if (photo.source !== undefined) { const sourceText = photo.source === 1 ? 'Strava' : photo.source === 2 ? 'Instagram' : `Unknown (${photo.source})`; details.push(` Source: ${sourceText}`); } // Add caption if available if (photo.caption) { details.push(` Caption: ${photo.caption}`); } // Add location if available if (photo.location && photo.location.length === 2) { const lat = photo.location[0]; const lng = photo.location[1]; if (lat !== undefined && lng !== undefined) { details.push(` Location: ${lat.toFixed(6)}, ${lng.toFixed(6)}`); } } // Add timestamps if (photo.created_at) { details.push(` Created: ${photo.created_at}`); } // Add URLs if (photo.urls && Object.keys(photo.urls).length > 0) { details.push(` URLs:`); for (const [sizeKey, url] of Object.entries(photo.urls)) { details.push(` ${sizeKey}: ${url}`); } } return details.join('\n'); }); const summaryText = `Activity Photos (ID: ${activityId})\nTotal Photos: ${photos.length}\n\n${photoSummaries.join('\n\n')}`; // Add raw data section const rawDataText = `\n\nComplete Photo Data:\n${JSON.stringify(photos, null, 2)}`; console.error(`Successfully fetched ${photos.length} photos for activity ${activityId}`); return { content: [ { type: "text" as const, text: summaryText }, { type: "text" as const, text: rawDataText } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error fetching photos for activity ${id}: ${errorMessage}`); const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404") ? `Activity with ID ${id} not found.` : `An unexpected error occurred while fetching photos for activity ${id}. Details: ${errorMessage}`; return { content: [{ type: "text" as const, text: `Error: ${userFriendlyMessage}` }], isError: true }; } }
- src/tools/getActivityPhotos.ts:35-38 (schema)Zod input schema defining parameters for the tool: activity ID (number or string) and optional photo size.const inputSchema = z.object({ id: z.union([z.number(), z.string()]).describe("The identifier of the activity to fetch photos for."), size: z.number().int().positive().optional().describe("Optional photo size in pixels (e.g., 100, 600, 2048)."), });
- src/server.ts:173-178 (registration)Registration of the get-activity-photos tool with the MCP server using server.tool, referencing the tool object imported from ./tools/getActivityPhotos.js.server.tool( getActivityPhotosTool.name, getActivityPhotosTool.description, getActivityPhotosTool.inputSchema?.shape ?? {}, getActivityPhotosTool.execute );
- src/stravaClient.ts:1319-1359 (helper)The supporting client function getActivityPhotos that makes the actual Strava API call to /activities/{id}/photos, handles authentication, validation with Zod schema, and token refresh on errors.export async function getActivityPhotos( accessToken: string, activityId: number, size: number = 2048 ): Promise<StravaPhoto[]> { if (!accessToken) { throw new Error("Strava access token is required."); } if (!activityId) { throw new Error("Activity ID is required to fetch photos."); } // photo_sources=true is required to get native Strava photos (not just Instagram) // size parameter is required to get actual URLs instead of placeholders const params: Record<string, any> = { photo_sources: true, size: size }; try { const response = await stravaApi.get<unknown>(`activities/${activityId}/photos`, { headers: { Authorization: `Bearer ${accessToken}` }, params: params }); const validationResult = StravaPhotosResponseSchema.safeParse(response.data); if (!validationResult.success) { console.error(`Strava API validation failed (getActivityPhotos: ${activityId}):`, JSON.stringify(validationResult.error.errors, null, 2)); throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`); } return validationResult.data; } catch (error) { return await handleApiError<StravaPhoto[]>(error, `getActivityPhotos for ID ${activityId}`, async () => { // Use new token from environment after refresh const newToken = process.env.STRAVA_ACCESS_TOKEN!; return getActivityPhotos(newToken, activityId, size); }); } }