Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/api/src/routes/trails/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,9 @@ export const trailsRoutes = new Elysia({ prefix: '/trails' })
return status(422, { error: 'Could not extract trail metadata from page' });
}

return { title, description, image, url: response.url };
// response.url is empty string in test environments and some CF Worker contexts;
// fall back to the validated parsed.href rather than crashing on new URL("").
return { title, description, image, url: response.url || parsed.href };
} catch (error) {
if (error instanceof Error && error.name === 'TimeoutError') {
return status(504, { error: 'AllTrails request timed out' });
Expand Down
98 changes: 1 addition & 97 deletions packages/mcp/src/tools/trails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,102 +113,6 @@ export function registerTrailTools(agent: AgentContext): void {

// ── AllTrails preview ─────────────────────────────────────────────────────

agent.server.registerTool(
'search_trails',
{
description:
'Search for hiking, cycling, skiing, or other outdoor trails via OpenStreetMap. ' +
'Filter by name and/or location. Requires either q (text) or lat+lon (spatial search).',
inputSchema: {
q: z
.string()
.optional()
.describe('Trail or route name to search for (e.g. "John Muir Trail")'),
lat: z.number().optional().describe('Latitude of the center point for spatial search'),
lon: z.number().optional().describe('Longitude of the center point for spatial search'),
radius: z
.number()
.min(0)
.max(500)
.optional()
.describe('Search radius in kilometres (default 50, max 500)'),
sport: z
.enum(['hiking', 'cycling', 'skiing', 'running', 'horse_riding'])
.optional()
.describe('Filter by sport/activity type'),
limit: z
.number()
.int()
.min(1)
.max(200)
.default(50)
.describe('Maximum results to return (default 50)'),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe('Offset for client-side pagination (default 0)'),
},
},
async ({ q, lat, lon, radius, sport, limit, offset }) => {
try {
const data = await agent.api.get('/trails/search', {
q,
lat,
lon,
radius,
sport,
limit,
offset,
});
return ok(data);
} catch (e) {
return err(e);
}
},
);

agent.server.registerTool(
'get_trail',
{
description:
'Get metadata for a specific trail by its OpenStreetMap relation ID. ' +
'Returns name, sport, network, distance, difficulty, and bounding box.',
inputSchema: {
osmId: z.string().describe('OSM relation ID of the trail (e.g. "1243746")'),
},
},
async ({ osmId }) => {
try {
const data = await agent.api.get(`/trails/${osmId}`);
return ok(data);
} catch (e) {
return err(e);
}
},
);

agent.server.registerTool(
'get_trail_geometry',
{
description:
'Get full GeoJSON geometry for a trail by its OpenStreetMap relation ID. ' +
'Returns all trail metadata plus a GeoJSON LineString or MultiLineString.',
inputSchema: {
osmId: z.string().describe('OSM relation ID of the trail (e.g. "1243746")'),
},
},
async ({ osmId }) => {
try {
const data = await agent.api.get(`/trails/${osmId}/geometry`);
return ok(data);
} catch (e) {
return err(e);
}
},
);

agent.server.registerTool(
'preview_alltrails_url',
{
Expand All @@ -223,7 +127,7 @@ export function registerTrailTools(agent: AgentContext): void {
},
async ({ url }) => {
try {
const data = await agent.api.post('/alltrails/preview', { url });
const data = await agent.api.post('/trails/alltrails-preview', { url });
return ok(data);
} catch (e) {
return err(e);
Expand Down
Loading