catence 0.1.0 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -293
- package/config.example.json +7 -20
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/runtime/catalog.d.ts +41 -0
- package/dist/core/runtime/catalog.js +111 -0
- package/dist/core/runtime/configuration.d.ts +1 -1
- package/dist/core/runtime/configuration.js +3 -3
- package/dist/core/runtime/secrets.d.ts +24 -0
- package/dist/core/runtime/secrets.js +68 -0
- package/dist/elt/application/management.js +4 -2
- package/dist/elt/ingestion/providers/strava/service.js +2 -1
- package/dist/interfaces/cli/commands.js +111 -46
- package/dist/interfaces/http/cli-options.d.ts +2 -2
- package/dist/interfaces/http/cli-options.js +10 -10
- package/dist/interfaces/http/server.d.ts +4 -1
- package/dist/interfaces/http/server.js +31 -4
- package/dist/interfaces/mcp/cli-options.d.ts +2 -2
- package/dist/interfaces/mcp/cli-options.js +14 -14
- package/dist/interfaces/mcp/main.js +12 -5
- package/dist/interfaces/mcp/server.d.ts +2 -2
- package/dist/interfaces/mcp/server.js +149 -98
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/docs/llm-providers.md +237 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type McpCliOptions = {
|
|
2
|
-
|
|
2
|
+
home?: string;
|
|
3
3
|
help: boolean;
|
|
4
4
|
};
|
|
5
|
-
export declare const MCP_USAGE = "Usage: catence [--
|
|
5
|
+
export declare const MCP_USAGE = "Usage: catence [--home <directory>]\n catence demo [--home <directory>]\n\nRun Catence as a local, stdio-only MCP server. Ordinary data access is read-only; only declared Strava hydration tools write through the locked local store.\n\nThe demo command creates safe generated data in ~/.catence-demo by default, then starts the same shared-athlete MCP server. It refuses to overwrite a non-demo athlete store.\n\nOptions:\n --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.\n -h, --help Show this help text.";
|
|
6
6
|
export declare function parseMcpCliOptions(arguments_: readonly string[]): McpCliOptions;
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
export const MCP_USAGE = `Usage: catence [--
|
|
2
|
-
catence demo [--
|
|
1
|
+
export const MCP_USAGE = `Usage: catence [--home <directory>]
|
|
2
|
+
catence demo [--home <directory>]
|
|
3
3
|
|
|
4
4
|
Run Catence as a local, stdio-only MCP server. Ordinary data access is read-only; only declared Strava hydration tools write through the locked local store.
|
|
5
5
|
|
|
6
|
-
The demo command creates safe generated data in
|
|
6
|
+
The demo command creates safe generated data in ~/.catence-demo by default, then starts the same shared-athlete MCP server. It refuses to overwrite a non-demo athlete store.
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
--
|
|
9
|
+
--home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.
|
|
10
10
|
-h, --help Show this help text.`;
|
|
11
11
|
export function parseMcpCliOptions(arguments_) {
|
|
12
|
-
let
|
|
12
|
+
let home;
|
|
13
13
|
for (let index = 0; index < arguments_.length; index++) {
|
|
14
14
|
const argument = arguments_[index];
|
|
15
15
|
if (argument === '-h' || argument === '--help')
|
|
16
|
-
return {
|
|
17
|
-
if (argument === '--
|
|
16
|
+
return { home, help: true };
|
|
17
|
+
if (argument === '--home') {
|
|
18
18
|
const value = arguments_[index + 1];
|
|
19
19
|
if (!value || value.startsWith('-'))
|
|
20
|
-
throw new Error('--
|
|
21
|
-
|
|
20
|
+
throw new Error('--home requires a directory.');
|
|
21
|
+
home = value;
|
|
22
22
|
index++;
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
|
-
if (argument.startsWith('--
|
|
26
|
-
const value = argument.slice('--
|
|
25
|
+
if (argument.startsWith('--home=')) {
|
|
26
|
+
const value = argument.slice('--home='.length);
|
|
27
27
|
if (!value)
|
|
28
|
-
throw new Error('--
|
|
29
|
-
|
|
28
|
+
throw new Error('--home requires a directory.');
|
|
29
|
+
home = value;
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
throw new Error(`Unknown option: ${argument}`);
|
|
33
33
|
}
|
|
34
|
-
return {
|
|
34
|
+
return { home, help: false };
|
|
35
35
|
}
|
|
36
36
|
//# sourceMappingURL=cli-options.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { addAthlete, athleteStorePaths, createDemoStore, defaultCatalogHome, initializeCatalog, loadCatalog, resolveCatalogPaths } from '../../runtime/index.js';
|
|
4
5
|
import { parseServeCliOptions, SERVE_USAGE } from '../http/cli-options.js';
|
|
5
6
|
import { createCatenceHttpServer } from '../http/server.js';
|
|
6
7
|
import { MCP_USAGE, parseMcpCliOptions } from './cli-options.js';
|
|
@@ -14,7 +15,7 @@ async function run() {
|
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
const httpServer = createCatenceHttpServer({
|
|
17
|
-
|
|
18
|
+
catalogPaths: resolveCatalogPaths(options.home),
|
|
18
19
|
allowedOrigins: options.allowedOrigins,
|
|
19
20
|
});
|
|
20
21
|
await new Promise((resolve, reject) => {
|
|
@@ -38,13 +39,19 @@ async function run() {
|
|
|
38
39
|
process.stderr.write(`${MCP_USAGE}\n`);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
const
|
|
42
|
-
const paths = resolvePaths(dataDir);
|
|
42
|
+
const catalogPaths = resolveCatalogPaths(demo && !options.home ? `${defaultCatalogHome()}-demo` : options.home);
|
|
43
43
|
if (demo) {
|
|
44
|
+
if (!existsSync(catalogPaths.catalog)) {
|
|
45
|
+
await initializeCatalog(catalogPaths, { id: 'demo', label: 'Generated demo athlete' });
|
|
46
|
+
}
|
|
47
|
+
else if (!(await loadCatalog(catalogPaths)).athletes.some((athlete) => athlete.id === 'demo')) {
|
|
48
|
+
await addAthlete(catalogPaths, { id: 'demo', label: 'Generated demo athlete' });
|
|
49
|
+
}
|
|
50
|
+
const paths = athleteStorePaths(catalogPaths, 'demo');
|
|
44
51
|
const result = await createDemoStore(paths);
|
|
45
52
|
process.stderr.write(`${String(result.message)}\n`);
|
|
46
53
|
}
|
|
47
|
-
const server = createCatenceMcpServer(
|
|
54
|
+
const server = createCatenceMcpServer(catalogPaths);
|
|
48
55
|
await server.connect(new StdioServerTransport());
|
|
49
56
|
console.error('Catence MCP server is running on stdio; ordinary tools are read-only and Strava hydration is lock-guarded.');
|
|
50
57
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { hydrateStravaActivity, type CatencePaths } from '../../runtime/index.js';
|
|
2
|
+
import { type CatalogPaths, hydrateStravaActivity, type CatencePaths } from '../../runtime/index.js';
|
|
3
3
|
type StravaActivityHydrator = typeof hydrateStravaActivity;
|
|
4
4
|
type McpDependencies = {
|
|
5
5
|
hydrateStravaActivity?: StravaActivityHydrator;
|
|
6
6
|
};
|
|
7
|
-
export declare function createCatenceMcpServer(paths?: CatencePaths, dependencies?: McpDependencies): McpServer;
|
|
7
|
+
export declare function createCatenceMcpServer(paths?: CatencePaths | CatalogPaths, dependencies?: McpDependencies): McpServer;
|
|
8
8
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
3
|
import { z } from 'zod';
|
|
3
|
-
import { ActivityDiscoveryService, AnalyticsService, configuredMcpRateLimit, DataWriteBusyError, DATASET_CATALOG, demoStoreMetadata, FitnessService, getDataset, hydrateStravaActivity, hydrateStravaSegmentHistory, jsonSafe, loadCatenceConfig, openReadOnlyRepository, queryReadOnlyData, QueryValidationError, ReadOnlyDatabaseError,
|
|
4
|
+
import { ActivityDiscoveryService, AnalyticsService, loadCatalog, configuredMcpRateLimit, DataWriteBusyError, DATASET_CATALOG, demoStoreMetadata, FitnessService, getDataset, hydrateStravaActivity, hydrateStravaSegmentHistory, jsonSafe, loadCatenceConfig, openReadOnlyRepository, queryReadOnlyData, QueryValidationError, ReadOnlyDatabaseError, resolveAthlete, resolveCatalogPaths, searchContext, SlidingWindowLimiter, StravaEnrichmentError, StravaRateLimitError, SwimmingService, WELLNESS_METRICS, WellnessService, } from '../../runtime/index.js';
|
|
4
5
|
const filterSchema = z.object({
|
|
5
6
|
column: z.string().min(1),
|
|
6
7
|
op: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'between', 'contains']),
|
|
@@ -18,6 +19,9 @@ const seriesInput = {
|
|
|
18
19
|
cursor: z.string().min(1).optional(),
|
|
19
20
|
pageSize: z.number().int().min(1).max(1_000).optional(),
|
|
20
21
|
};
|
|
22
|
+
function isCatalogPaths(value) {
|
|
23
|
+
return 'catalog' in value;
|
|
24
|
+
}
|
|
21
25
|
const MCP_INSTRUCTIONS = [
|
|
22
26
|
'For a selected activity\'s Strava segments, climb segments, grade by segment, KOM/PR, or per-segment analysis, call get_activity_segments after identifying the activity.',
|
|
23
27
|
'That tool performs the targeted Strava hydration itself. Do not say segment data is unavailable before it returns; if it reports not_found, ambiguous, authorization, throttling, or an error, report that exact outcome instead.',
|
|
@@ -116,29 +120,63 @@ async function activitySegments(repository, activityId, hydratedStravaActivityId
|
|
|
116
120
|
LIMIT $limit
|
|
117
121
|
`, values);
|
|
118
122
|
}
|
|
119
|
-
export function createCatenceMcpServer(paths =
|
|
123
|
+
export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependencies = {}) {
|
|
120
124
|
const server = new McpServer({ name: 'catence', version: '0.1.0' }, { instructions: MCP_INSTRUCTIONS });
|
|
121
125
|
const hydrateActivity = dependencies.hydrateStravaActivity ?? hydrateStravaActivity;
|
|
122
|
-
const
|
|
123
|
-
const
|
|
126
|
+
const catalogPaths = isCatalogPaths(paths) ? paths : null;
|
|
127
|
+
const staticPaths = isCatalogPaths(paths) ? null : paths;
|
|
128
|
+
const scope = new AsyncLocalStorage();
|
|
129
|
+
const activePaths = () => scope.getStore()?.paths ?? staticPaths ?? (() => { throw new Error('athleteId is required. Call list_athletes to inspect the configured catalog.'); })();
|
|
130
|
+
const activeAthlete = () => scope.getStore()?.athlete;
|
|
124
131
|
const limiter = new SlidingWindowLimiter();
|
|
125
|
-
const
|
|
132
|
+
const athleteIdSchema = z.string().regex(/^[a-z][a-z0-9-]{0,62}$/);
|
|
133
|
+
const promptAthleteSchema = catalogPaths ? { athleteId: athleteIdSchema } : {};
|
|
134
|
+
if (catalogPaths) {
|
|
135
|
+
server.registerTool('list_athletes', {
|
|
136
|
+
title: 'List configured athletes',
|
|
137
|
+
description: 'List athlete IDs and labels available to this shared Catence agent. No personal metrics are returned.',
|
|
138
|
+
}, async () => {
|
|
139
|
+
try {
|
|
140
|
+
const catalog = await loadCatalog(catalogPaths);
|
|
141
|
+
return textResult({ data: { defaultAthleteId: catalog.defaultAthleteId, athletes: catalog.athletes } });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return errorResult(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const originalRegister = server.registerTool.bind(server);
|
|
148
|
+
server.registerTool = (name, configuration, handler) => {
|
|
149
|
+
const originalInput = configuration.inputSchema;
|
|
150
|
+
return originalRegister(name, {
|
|
151
|
+
...configuration,
|
|
152
|
+
description: `${String(configuration.description ?? '')} Requires athleteId from list_athletes.`,
|
|
153
|
+
inputSchema: { athleteId: athleteIdSchema, ...(originalInput ?? {}) },
|
|
154
|
+
}, handler);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const tool = (name, fn) => async (rawInput) => {
|
|
126
158
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
const selected = catalogPaths
|
|
160
|
+
? await resolveAthlete(catalogPaths, rawInput.athleteId ?? '')
|
|
161
|
+
: null;
|
|
162
|
+
const run = async () => {
|
|
163
|
+
const currentPaths = activePaths();
|
|
164
|
+
const config = await loadCatenceConfig(currentPaths);
|
|
165
|
+
const decision = limiter.check(`${activeAthlete()?.id ?? 'local'}:tool:${name}`, configuredMcpRateLimit(config, 'tools', name));
|
|
166
|
+
if (!decision.allowed)
|
|
167
|
+
return { ...textResult({ data: null, ...(activeAthlete() ? { athlete: activeAthlete() } : {}), error: { code: 'rate_limited', message: `MCP tool ${name} is locally rate limited.`, retryAfterSeconds: decision.retryAfterSeconds } }), isError: true };
|
|
168
|
+
const { athleteId: _athleteId, ...input } = rawInput;
|
|
169
|
+
const value = await fn(input);
|
|
170
|
+
const result = value && typeof value === 'object' && !Array.isArray(value) ? value : { data: value };
|
|
171
|
+
const demoStore = await demoStoreMetadata(currentPaths);
|
|
172
|
+
const caveats = Array.isArray(result.caveats) ? result.caveats : [];
|
|
173
|
+
return textResult({
|
|
174
|
+
...result,
|
|
175
|
+
...(activeAthlete() ? { athlete: activeAthlete() } : {}),
|
|
176
|
+
...(demoStore ? { demoStore: { generated: true, seed: demoStore.seed, days: demoStore.days, startDate: demoStore.startDate, endDate: demoStore.endDate }, caveats: [...caveats, 'This response uses Catence generated demo data, not personal measurements.'] } : {}),
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
return selected ? scope.run(selected, run) : run();
|
|
142
180
|
}
|
|
143
181
|
catch (error) {
|
|
144
182
|
return errorResult(error);
|
|
@@ -146,7 +184,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
146
184
|
};
|
|
147
185
|
const resourceLimit = async (name, uri) => {
|
|
148
186
|
try {
|
|
149
|
-
const config = await
|
|
187
|
+
const config = await loadCatenceConfig(activePaths());
|
|
150
188
|
const decision = limiter.check(`resource:${name}`, configuredMcpRateLimit(config, 'resources', name));
|
|
151
189
|
if (!decision.allowed)
|
|
152
190
|
return resource(uri, { error: { code: 'rate_limited', message: `MCP resource ${name} is locally rate limited.`, retryAfterSeconds: decision.retryAfterSeconds } });
|
|
@@ -159,37 +197,37 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
159
197
|
server.registerPrompt('daily_recovery_load_review', {
|
|
160
198
|
title: 'Daily recovery and load review',
|
|
161
199
|
description: 'Ask for a source-cited recovery and training-load review for one date.',
|
|
162
|
-
argsSchema: { date: z.string().date().optional() },
|
|
163
|
-
}, ({ date }) => ({
|
|
200
|
+
argsSchema: { ...promptAthleteSchema, date: z.string().date().optional() },
|
|
201
|
+
}, ({ date, ...input }) => ({
|
|
164
202
|
messages: [{
|
|
165
203
|
role: 'user',
|
|
166
|
-
content: { type: 'text', text: `Run review_daily_recovery_load for ${date ?? utcDate()}. Summarize only returned evidence, name data gaps, and do not present a training prescription as fact.` },
|
|
204
|
+
content: { type: 'text', text: `Run review_daily_recovery_load for ${date ?? utcDate()}${typeof input.athleteId === 'string' ? ` with athleteId ${input.athleteId}` : ''}. Summarize only returned evidence, name data gaps, and do not present a training prescription as fact.` },
|
|
167
205
|
}],
|
|
168
206
|
}));
|
|
169
207
|
server.registerPrompt('weekly_training_review', {
|
|
170
208
|
title: 'Weekly training review',
|
|
171
209
|
description: 'Ask for a source-cited seven-day training review ending on an optional date.',
|
|
172
|
-
argsSchema: { endDate: z.string().date().optional() },
|
|
173
|
-
}, ({ endDate }) => ({
|
|
210
|
+
argsSchema: { ...promptAthleteSchema, endDate: z.string().date().optional() },
|
|
211
|
+
}, ({ endDate, ...input }) => ({
|
|
174
212
|
messages: [{
|
|
175
213
|
role: 'user',
|
|
176
|
-
content: { type: 'text', text: `Run review_weekly_training ending on ${endDate ?? utcDate()}. Explain volume, load, and recovery evidence with dates and coverage caveats.` },
|
|
214
|
+
content: { type: 'text', text: `Run review_weekly_training ending on ${endDate ?? utcDate()}${typeof input.athleteId === 'string' ? ` with athleteId ${input.athleteId}` : ''}. Explain volume, load, and recovery evidence with dates and coverage caveats.` },
|
|
177
215
|
}],
|
|
178
216
|
}));
|
|
179
217
|
server.registerPrompt('activity_deep_dive', {
|
|
180
218
|
title: 'Activity deep dive',
|
|
181
219
|
description: 'Ask for an evidence-first deep dive into one canonical Catence activity.',
|
|
182
|
-
argsSchema: { activityId: z.string().min(1) },
|
|
183
|
-
}, ({ activityId }) => ({
|
|
220
|
+
argsSchema: { ...promptAthleteSchema, activityId: z.string().min(1) },
|
|
221
|
+
}, ({ activityId, ...input }) => ({
|
|
184
222
|
messages: [{
|
|
185
223
|
role: 'user',
|
|
186
|
-
content: { type: 'text', text: `Run review_activity_deep_dive for ${activityId}. Use its source records and intervals first; load read_series only if a specific sampled metric is needed.` },
|
|
224
|
+
content: { type: 'text', text: `Run review_activity_deep_dive for ${activityId}${typeof input.athleteId === 'string' ? ` with athleteId ${input.athleteId}` : ''}. Use its source records and intervals first; load read_series only if a specific sampled metric is needed.` },
|
|
187
225
|
}],
|
|
188
226
|
}));
|
|
189
227
|
server.registerTool('catence_status', {
|
|
190
228
|
title: 'Catence data status',
|
|
191
229
|
description: 'Read sync state, data coverage, entity counts, stream availability, unresolved errors, and retrieval-index freshness. Read-only.',
|
|
192
|
-
}, tool('catence_status', async () => useRepository(
|
|
230
|
+
}, tool('catence_status', async () => useRepository(activePaths(), async (repository) => ({
|
|
193
231
|
data: { ...(await repository.status()), ...(await repository.coverage()) },
|
|
194
232
|
provenance: { database: 'read-only DuckDB snapshot' }, query: {}, caveats: [],
|
|
195
233
|
}))));
|
|
@@ -199,7 +237,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
199
237
|
inputSchema: { date: z.string().date().optional() },
|
|
200
238
|
}, tool('review_daily_recovery_load', async (input) => {
|
|
201
239
|
const date = input.date ?? utcDate();
|
|
202
|
-
return useRepository(
|
|
240
|
+
return useRepository(activePaths(), async (repository) => ({
|
|
203
241
|
data: { date, ...(await repository.summary(date, date)) },
|
|
204
242
|
provenance: { relations: ['daily_health', 'canonical_activity_training', 'nutrition_days'], database: 'read-only DuckDB snapshot' },
|
|
205
243
|
query: { date },
|
|
@@ -216,7 +254,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
216
254
|
}, tool('review_weekly_training', async (input) => {
|
|
217
255
|
const endDate = input.endDate ?? utcDate();
|
|
218
256
|
const startDate = subtractDays(endDate, 6);
|
|
219
|
-
return useRepository(
|
|
257
|
+
return useRepository(activePaths(), async (repository) => ({
|
|
220
258
|
data: { startDate, endDate, ...(await repository.summary(startDate, endDate)) },
|
|
221
259
|
provenance: { relations: ['daily_health', 'canonical_activity_training', 'nutrition_days'], database: 'read-only DuckDB snapshot' },
|
|
222
260
|
query: { startDate, endDate },
|
|
@@ -230,7 +268,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
230
268
|
title: 'Review one activity in depth',
|
|
231
269
|
description: 'Return canonical activity identity, source summaries, and structured intervals. Call read_series separately only when a specific sampled metric is required.',
|
|
232
270
|
inputSchema: { activityId: z.string().min(1) },
|
|
233
|
-
}, tool('review_activity_deep_dive', async (input) => useRepository(
|
|
271
|
+
}, tool('review_activity_deep_dive', async (input) => useRepository(activePaths(), async (repository) => {
|
|
234
272
|
const activity = await repository.activity(input.activityId);
|
|
235
273
|
return {
|
|
236
274
|
data: activity,
|
|
@@ -244,7 +282,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
244
282
|
server.registerTool('describe_data', {
|
|
245
283
|
title: 'Describe available Catence datasets',
|
|
246
284
|
description: 'List cataloged datasets, fields, units, permitted filters/groupings, providers, and time coverage. Read-only.',
|
|
247
|
-
}, tool('describe_data', async () => useRepository(
|
|
285
|
+
}, tool('describe_data', async () => useRepository(activePaths(), async (repository) => ({
|
|
248
286
|
data: { datasets: repository.catalog(), ...(await repository.coverage()) },
|
|
249
287
|
provenance: { catalog: 'Catence catalog, restricted to analytical views' }, query: {}, caveats: ['activity_samples is available only through read_series or aggregate_data and only reads registered Parquet stream manifests.'],
|
|
250
288
|
}))));
|
|
@@ -252,7 +290,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
252
290
|
title: 'Describe one Catence dataset',
|
|
253
291
|
description: 'Read a compact schema, permitted filters/groupings, provenance fields, and coverage for one cataloged dataset. Read-only.',
|
|
254
292
|
inputSchema: { dataset: z.string().min(1) },
|
|
255
|
-
}, tool('describe_dataset', async (input) => useRepository(
|
|
293
|
+
}, tool('describe_dataset', async (input) => useRepository(activePaths(), async (repository) => {
|
|
256
294
|
const dataset = getDataset(input.dataset);
|
|
257
295
|
const coverage = (await repository.coverage()).coverage;
|
|
258
296
|
const observedValues = dataset.name === 'training_metric_observations'
|
|
@@ -273,7 +311,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
273
311
|
})));
|
|
274
312
|
server.registerTool('read_series', {
|
|
275
313
|
title: 'Read a bounded time series', description: 'Read cataloged numeric series with deterministic cursor pagination and automatic stream downsampling. metrics must be numeric catalog columns; place identifiers and other strings in filters. Call describe_dataset first if the fields are uncertain.', inputSchema: seriesInput,
|
|
276
|
-
}, tool('read_series', async (input) => useRepository(
|
|
314
|
+
}, tool('read_series', async (input) => useRepository(activePaths(), (repository) => new AnalyticsService(repository).readSeries({ ...input, filters: input.filters }))));
|
|
277
315
|
server.registerTool('aggregate_data', {
|
|
278
316
|
title: 'Aggregate cataloged data', description: 'Declarative aggregation over one cataloged dataset. A timeBucket adds a time_bucket field, which can be used in orderBy. No joins, arbitrary expressions, or file paths.',
|
|
279
317
|
inputSchema: {
|
|
@@ -281,15 +319,15 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
281
319
|
metrics: z.array(z.object({ column: z.string().min(1), operation: z.enum(['count', 'sum', 'mean', 'min', 'max', 'percentile']), percentile: z.number().gt(0).lt(1).optional(), as: z.string().min(1).max(64).optional() })).min(1).max(12),
|
|
282
320
|
dimensions: z.array(z.string().min(1)).max(8).optional(), filters: z.array(filterSchema).max(50).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional(), timeBucket: z.enum(['day', 'week', 'month']).optional(), orderBy: z.object({ column: z.string().min(1), direction: z.enum(['asc', 'desc']).optional() }).optional(), limit: z.number().int().min(1).max(500).optional(),
|
|
283
321
|
},
|
|
284
|
-
}, tool('aggregate_data', async (input) => useRepository(
|
|
322
|
+
}, tool('aggregate_data', async (input) => useRepository(activePaths(), (repository) => new AnalyticsService(repository).aggregate({ ...input, filters: input.filters }))));
|
|
285
323
|
server.registerTool('analyze_series', {
|
|
286
324
|
title: 'Analyze a descriptive series', description: 'Run deterministic rolling statistics, baselines, correlations, seasonal comparisons, or trends on a cataloged series.',
|
|
287
325
|
inputSchema: { ...seriesInput, analysis: z.enum(['rolling_mean', 'rolling_median', 'baseline_change', 'z_score', 'pearson', 'spearman', 'seasonal_comparison', 'linear_trend', 'theil_sen_trend']), compareMetric: z.string().min(1).optional(), window: z.number().int().min(2).max(100).optional() },
|
|
288
|
-
}, tool('analyze_series', async (input) => useRepository(
|
|
326
|
+
}, tool('analyze_series', async (input) => useRepository(activePaths(), (repository) => new AnalyticsService(repository).analyzeSeries({ ...input, filters: input.filters }))));
|
|
289
327
|
server.registerTool('fit_series_model', {
|
|
290
328
|
title: 'Fit a descriptive series model', description: 'Fit a bounded OLS, Theil–Sen, quadratic, or cubic descriptive model. Not a sport-performance model.',
|
|
291
329
|
inputSchema: { ...seriesInput, model: z.enum(['ols_linear', 'theil_sen_linear', 'polynomial_2', 'polynomial_3']), xMetric: z.string().min(1).optional(), yMetric: z.string().min(1).optional() },
|
|
292
|
-
}, tool('fit_series_model', async (input) => useRepository(
|
|
330
|
+
}, tool('fit_series_model', async (input) => useRepository(activePaths(), (repository) => new AnalyticsService(repository).fitSeriesModel({ ...input, filters: input.filters }))));
|
|
293
331
|
server.registerTool('wellness_correlate', {
|
|
294
332
|
title: 'Correlate recovery and training metrics',
|
|
295
333
|
description: 'Calculate a compact daily Pearson or Spearman correlation between curated wellness and training metrics, with an optional -7 through +7 day lag scan. Descriptive only.',
|
|
@@ -297,32 +335,32 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
297
335
|
metricA: z.enum(WELLNESS_METRICS), metricB: z.enum(WELLNESS_METRICS), startDate: z.string().date().optional(), endDate: z.string().date().optional(),
|
|
298
336
|
method: z.enum(['pearson', 'spearman']).optional(), lagDays: z.number().int().min(-30).max(30).optional(), scanLags: z.boolean().optional(),
|
|
299
337
|
},
|
|
300
|
-
}, tool('wellness_correlate', async (input) => useRepository(
|
|
338
|
+
}, tool('wellness_correlate', async (input) => useRepository(activePaths(), (repository) => new WellnessService(repository).correlate(input))));
|
|
301
339
|
server.registerTool('wellness_baselines', {
|
|
302
340
|
title: 'Read personal wellness baselines',
|
|
303
341
|
description: 'Return trailing means, standard-deviation bands, latest values, and latest z-scores for common recovery and training metrics. Missing values remain missing.',
|
|
304
342
|
inputSchema: { metrics: z.array(z.enum(WELLNESS_METRICS)).min(1).max(12).optional(), endDate: z.string().date().optional(), windowDays: z.number().int().min(7).max(365).optional() },
|
|
305
|
-
}, tool('wellness_baselines', async (input) => useRepository(
|
|
343
|
+
}, tool('wellness_baselines', async (input) => useRepository(activePaths(), (repository) => new WellnessService(repository).baselines(input))));
|
|
306
344
|
server.registerTool('wellness_anomalies', {
|
|
307
345
|
title: 'Find statistical wellness anomalies',
|
|
308
346
|
description: 'Find daily recovery and wellness outliers by z-score and group dates with anomalies across multiple signals. This does not diagnose or prescribe.',
|
|
309
347
|
inputSchema: { metrics: z.array(z.enum(WELLNESS_METRICS)).min(1).max(12).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional(), zThreshold: z.number().min(1).max(5).optional() },
|
|
310
|
-
}, tool('wellness_anomalies', async (input) => useRepository(
|
|
348
|
+
}, tool('wellness_anomalies', async (input) => useRepository(activePaths(), (repository) => new WellnessService(repository).anomalies(input))));
|
|
311
349
|
server.registerTool('wellness_coverage', {
|
|
312
350
|
title: 'Inspect wellness data coverage',
|
|
313
351
|
description: 'Report present and missing dates for curated wellness/training metrics, plus unresolved extraction errors. Missing data is never interpreted as rest or a health outcome.',
|
|
314
352
|
inputSchema: { metrics: z.array(z.enum(WELLNESS_METRICS)).min(1).max(12).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional() },
|
|
315
|
-
}, tool('wellness_coverage', async (input) => useRepository(
|
|
353
|
+
}, tool('wellness_coverage', async (input) => useRepository(activePaths(), (repository) => new WellnessService(repository).coverage(input))));
|
|
316
354
|
server.registerTool('get_ftp_history', {
|
|
317
355
|
title: 'Get dated FTP history',
|
|
318
356
|
description: 'Return normalized cycling FTP settings and activity-summary observations with source-aware preferred daily values. Read-only.',
|
|
319
357
|
inputSchema: { sport: z.string().min(1).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional(), sourcePreference: z.enum(['settings', 'settings_then_activity', 'all']).optional() },
|
|
320
|
-
}, tool('get_ftp_history', async (input) => useRepository(
|
|
358
|
+
}, tool('get_ftp_history', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).ftpHistory(input))));
|
|
321
359
|
server.registerTool('get_vo2max_history', {
|
|
322
360
|
title: 'Get sport-specific VO₂max history',
|
|
323
361
|
description: 'Return normalized Garmin VO₂max observations for exactly one sport. For running, pass sport: running (or run): Garmin supplies that series with raw sport generic, which is preserved in each row. Cycling remains separate. Omit sport only to inspect available source labels. Read-only.',
|
|
324
362
|
inputSchema: { sport: z.string().min(1).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional() },
|
|
325
|
-
}, tool('get_vo2max_history', async (input) => useRepository(
|
|
363
|
+
}, tool('get_vo2max_history', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).vo2MaxHistory(input))));
|
|
326
364
|
server.registerTool('find_activities', {
|
|
327
365
|
title: 'Find activities and likely races',
|
|
328
366
|
description: 'Find canonical activities by sport, distance, name, and date. Results are paginated and transparently flag likely-race signals without claiming provider-confirmed race metadata. Read-only.',
|
|
@@ -333,25 +371,25 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
333
371
|
startDate: z.string().date().optional(), endDate: z.string().date().optional(),
|
|
334
372
|
sort: z.enum(['date_desc', 'date_asc']).optional(), limit: z.number().int().min(1).max(100).optional(), cursor: z.string().min(1).optional(),
|
|
335
373
|
},
|
|
336
|
-
}, tool('find_activities', async (input) => useRepository(
|
|
374
|
+
}, tool('find_activities', async (input) => useRepository(activePaths(), (repository) => new ActivityDiscoveryService(repository).findActivities(input))));
|
|
337
375
|
server.registerTool('get_swim_laps', {
|
|
338
376
|
title: 'Read explicit swim lengths and grouped sets',
|
|
339
377
|
description: 'Return source-aware swim lengths when a provider actually supplied them, plus Garmin detected and Intervals.icu auto-detected sets. A missing length list is reported as unavailable; laps are never reconstructed from samples.',
|
|
340
378
|
inputSchema: { activityId: z.string().min(1), provider: z.enum(['garmin', 'intervals']).optional() },
|
|
341
|
-
}, tool('get_swim_laps', async (input) => useRepository(
|
|
379
|
+
}, tool('get_swim_laps', async (input) => useRepository(activePaths(), (repository) => new SwimmingService(repository).swimLaps(input))));
|
|
342
380
|
server.registerTool('swim_progress_report', {
|
|
343
381
|
title: 'Build a source-aware swim progress report',
|
|
344
382
|
description: 'Compare Garmin swimming summaries and data completeness over a date range, optionally within one pool length. It does not derive pace trends from moving time divided by distance.',
|
|
345
383
|
inputSchema: { startDate: z.string().date().optional(), endDate: z.string().date().optional(), poolLengthM: z.number().positive().max(200).optional() },
|
|
346
|
-
}, tool('swim_progress_report', async (input) => useRepository(
|
|
384
|
+
}, tool('swim_progress_report', async (input) => useRepository(activePaths(), (repository) => new SwimmingService(repository).swimProgressReport(input))));
|
|
347
385
|
server.registerTool('get_activity_segments', {
|
|
348
386
|
title: 'Hydrate and read an activity’s Strava segments',
|
|
349
387
|
description: 'For a selected activity’s segments, climbs, KOM/PRs, or per-segment analysis: automatically hydrate the matching Strava activity before returning its persisted segment efforts. Use this before saying segment data is unavailable.',
|
|
350
388
|
inputSchema: { activityId: z.string().min(1), refresh: z.boolean().optional(), limit: z.number().int().min(1).max(500).optional() },
|
|
351
389
|
}, tool('get_activity_segments', async (input) => {
|
|
352
|
-
const hydration = await hydrateActivity(
|
|
390
|
+
const hydration = await hydrateActivity(activePaths(), input.activityId, input.refresh ?? false);
|
|
353
391
|
const hydratedStravaActivityIds = stravaActivityIds(hydration);
|
|
354
|
-
const segments = await useRepository(
|
|
392
|
+
const segments = await useRepository(activePaths(), (repository) => activitySegments(repository, input.activityId, hydratedStravaActivityIds, input.limit ?? 200));
|
|
355
393
|
return {
|
|
356
394
|
data: { hydration, hydratedStravaActivityIds, segments },
|
|
357
395
|
provenance: { provider: 'strava', relations: ['activity_segments', 'strava_segments'], operation: 'targeted_activity_hydration_then_read' },
|
|
@@ -366,37 +404,37 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
366
404
|
title: 'Read labelled power-curve trends',
|
|
367
405
|
description: 'Return monthly bests for selected power durations from an explicit sport or sport family, including the supporting activity and source type. Read-only.',
|
|
368
406
|
inputSchema: { sport: z.string().min(1).optional(), sportFamily: z.enum(['cycling', 'running']).optional(), durations: z.array(z.number().int().min(1).max(86_400)).min(1).max(12).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional(), sourceQuality: z.enum(['all', 'garmin_fit_derived']).optional() },
|
|
369
|
-
}, tool('power_curve_trend', async (input) => useRepository(
|
|
407
|
+
}, tool('power_curve_trend', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).powerCurveTrend(input))));
|
|
370
408
|
server.registerTool('power_coverage_report', {
|
|
371
409
|
title: 'Inspect FIT-derived power coverage',
|
|
372
410
|
description: 'Report powered activities and the complete duration-best inventory for an explicit sport or sport family. Uses Garmin FIT-derived power, not sparse activity summaries. Read-only.',
|
|
373
411
|
inputSchema: { sport: z.string().min(1).optional(), sportFamily: z.enum(['cycling', 'running']).optional(), startDate: z.string().date().optional(), endDate: z.string().date().optional(), sourceQuality: z.enum(['all', 'garmin_fit_derived']).optional() },
|
|
374
|
-
}, tool('power_coverage_report', async (input) => useRepository(
|
|
412
|
+
}, tool('power_coverage_report', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).powerCoverageReport(input))));
|
|
375
413
|
server.registerTool('latest_cycling_activities', {
|
|
376
414
|
title: 'List latest cycling activities',
|
|
377
415
|
description: 'List Garmin cycling source records without duplicate Intervals summaries, optionally flagging multisport parent records. Read-only.',
|
|
378
416
|
inputSchema: { startDate: z.string().date().optional(), endDate: z.string().date().optional(), includeMultisport: z.boolean().optional(), limit: z.number().int().min(1).max(100).optional() },
|
|
379
|
-
}, tool('latest_cycling_activities', async (input) => useRepository(
|
|
417
|
+
}, tool('latest_cycling_activities', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).latestCyclingActivities(input))));
|
|
380
418
|
server.registerTool('cycling_progress_report', {
|
|
381
419
|
title: 'Build a cycling progress report',
|
|
382
420
|
description: 'Combine source-aware FTP and VO₂max histories with monthly canonical volume/load and labelled power-curve trends. Read-only and descriptive.',
|
|
383
421
|
inputSchema: { startDate: z.string().date().optional(), endDate: z.string().date().optional() },
|
|
384
|
-
}, tool('cycling_progress_report', async (input) => useRepository(
|
|
422
|
+
}, tool('cycling_progress_report', async (input) => useRepository(activePaths(), (repository) => new FitnessService(repository).cyclingProgressReport(input))));
|
|
385
423
|
server.registerTool('query_read_only_data', {
|
|
386
424
|
title: 'Query cataloged read-only data', description: 'Advanced fallback after describe_data/describe_dataset: one parameterized SELECT or WITH … SELECT over cataloged views only. Do not query information_schema, DuckDB system tables, or uncataloged physical relations. Results use deterministic cursor pagination and always report whether they are incomplete. Filesystem access, extensions, DDL, and mutation are rejected.',
|
|
387
425
|
inputSchema: { sql: z.string().min(1).max(20_000), values: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).refine((value) => Object.keys(value).length <= 100, 'At most 100 bind values are allowed.').optional(), cursor: z.string().min(1).optional(), pageSize: z.number().int().min(1).max(500).optional() },
|
|
388
|
-
}, tool('query_read_only_data', async (input) => useRepository(
|
|
426
|
+
}, tool('query_read_only_data', async (input) => useRepository(activePaths(), (repository) => queryReadOnlyData(repository, input))));
|
|
389
427
|
server.registerTool('search_context', {
|
|
390
428
|
title: 'Search generated context', description: 'Search compact generated activity, plan, nutrition, and message context. Results identify authoritative follow-up tools rather than making numerical claims.',
|
|
391
429
|
inputSchema: { query: z.string().min(2).max(500), filters: z.array(z.object({ column: z.enum(['provider', 'entity_type', 'occurred_on']), op: z.enum(['eq', 'in', 'between']), value: z.unknown() })).max(20).optional(), limit: z.number().int().min(1).max(50).optional() },
|
|
392
|
-
}, tool('search_context', async (input) => useRepository(
|
|
430
|
+
}, tool('search_context', async (input) => useRepository(activePaths(), (repository) => searchContext(repository, input))));
|
|
393
431
|
server.registerTool('hydrate_strava_activity', {
|
|
394
432
|
title: 'Hydrate one activity from Strava',
|
|
395
433
|
description: 'Write-only targeted enrichment. Safely matches exactly one Catence activity/source to Strava, then archives and persists its activity detail, gear assignment, and segment efforts. For a segment/climb request, prefer get_activity_segments, which invokes this prerequisite automatically.',
|
|
396
434
|
inputSchema: { activityId: z.string().min(1), refresh: z.boolean().optional() },
|
|
397
435
|
}, tool('hydrate_strava_activity', async (input) => {
|
|
398
|
-
const data = await hydrateActivity(
|
|
399
|
-
return { data, coverage: await useRepository(
|
|
436
|
+
const data = await hydrateActivity(activePaths(), input.activityId, input.refresh ?? false);
|
|
437
|
+
return { data, coverage: await useRepository(activePaths(), (repository) => repository.coverage()), provenance: { provider: 'strava', operation: 'activity_detail', archivedBeforeNormalization: true }, caveats: ['Segment verification is unavailable from Strava public segment data and is never inferred.'] };
|
|
400
438
|
}));
|
|
401
439
|
server.registerTool('hydrate_recent_strava_activities', {
|
|
402
440
|
title: 'Hydrate several recent Strava activities',
|
|
@@ -409,7 +447,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
409
447
|
}, tool('hydrate_recent_strava_activities', async (input) => {
|
|
410
448
|
const activityIds = input.activityIds
|
|
411
449
|
? [...new Set(input.activityIds)]
|
|
412
|
-
: await useRepository(
|
|
450
|
+
: await useRepository(activePaths(), async (repository) => {
|
|
413
451
|
const values = {
|
|
414
452
|
startDate: input.startDate, endDate: `${input.endDate}T23:59:59.999Z`, limit: input.limit ?? 20,
|
|
415
453
|
};
|
|
@@ -443,7 +481,7 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
443
481
|
continue;
|
|
444
482
|
}
|
|
445
483
|
try {
|
|
446
|
-
const result = await hydrateStravaActivity(
|
|
484
|
+
const result = await hydrateStravaActivity(activePaths(), activityId, input.refresh ?? false);
|
|
447
485
|
const status = typeof result.status === 'string' ? result.status : 'completed';
|
|
448
486
|
outcomes.push({ activityId, status, result });
|
|
449
487
|
}
|
|
@@ -471,44 +509,57 @@ export function createCatenceMcpServer(paths = resolvePaths(), dependencies = {}
|
|
|
471
509
|
description: 'Write-only targeted enrichment. Fetches a persisted Strava segment and the authenticated athlete’s paged historic efforts; interrupted or throttled work is resumable.',
|
|
472
510
|
inputSchema: { segmentId: z.string().min(1), refresh: z.boolean().optional() },
|
|
473
511
|
}, tool('hydrate_strava_segment_history', async (input) => {
|
|
474
|
-
const data = await hydrateStravaSegmentHistory(
|
|
475
|
-
return { data, coverage: await useRepository(
|
|
512
|
+
const data = await hydrateStravaSegmentHistory(activePaths(), input.segmentId, input.refresh ?? false);
|
|
513
|
+
return { data, coverage: await useRepository(activePaths(), (repository) => repository.coverage()), provenance: { provider: 'strava', operation: 'segment_effort_history', archivedBeforeNormalization: true }, caveats: ['Only the authenticated athlete’s historic segment efforts are requested.'] };
|
|
476
514
|
}));
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
515
|
+
if (catalogPaths) {
|
|
516
|
+
server.registerResource('athletes', 'catence://athletes', { title: 'Configured athlete roster', mimeType: 'application/json' }, async (uri) => {
|
|
517
|
+
try {
|
|
518
|
+
const catalog = await loadCatalog(catalogPaths);
|
|
519
|
+
return resource(uri, { defaultAthleteId: catalog.defaultAthleteId, athletes: catalog.athletes });
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
return resource(uri, { error: errorResult(error).content[0].text });
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
server.registerResource('status', 'catence://status', { title: 'Catence status', mimeType: 'application/json' }, async (uri) => {
|
|
528
|
+
const limited = await resourceLimit('status', uri);
|
|
529
|
+
if (limited)
|
|
530
|
+
return limited;
|
|
531
|
+
try {
|
|
532
|
+
return resource(uri, await useRepository(activePaths(), (repository) => repository.status()));
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return resource(uri, { error: errorResult(error).content[0].text });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
server.registerResource('catalog', 'catence://catalog', { title: 'Catence catalog', mimeType: 'application/json' }, async (uri) => { const limited = await resourceLimit('catalog', uri); if (limited)
|
|
539
|
+
return limited; return resource(uri, { datasets: Object.values(DATASET_CATALOG) }); });
|
|
540
|
+
server.registerResource('activity', new ResourceTemplate('catence://activity/{activityId}', { list: undefined }), { title: 'Activity detail', mimeType: 'application/json' }, async (uri, variables) => {
|
|
541
|
+
const limited = await resourceLimit('activity', uri);
|
|
542
|
+
if (limited)
|
|
543
|
+
return limited;
|
|
544
|
+
try {
|
|
545
|
+
return resource(uri, await useRepository(activePaths(), (repository) => repository.activity(variable(variables.activityId))));
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
return resource(uri, { error: errorResult(error).content[0].text });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
server.registerResource('summary', new ResourceTemplate('catence://summary/{startDate}/{endDate}', { list: undefined }), { title: 'Catence date-range summary', mimeType: 'application/json' }, async (uri, variables) => {
|
|
552
|
+
const limited = await resourceLimit('summary', uri);
|
|
553
|
+
if (limited)
|
|
554
|
+
return limited;
|
|
555
|
+
try {
|
|
556
|
+
return resource(uri, await useRepository(activePaths(), (repository) => repository.summary(variable(variables.startDate), variable(variables.endDate))));
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
return resource(uri, { error: errorResult(error).content[0].text });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
512
563
|
return server;
|
|
513
564
|
}
|
|
514
565
|
function variable(value) {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
export type { CatencePaths } from '../contracts/runtime.js';
|
|
10
10
|
export { CATENCE_PROTOCOL_VERSION, CATENCE_RUNTIME_VERSION, catenceRuntimeHealth } from '../contracts/release.js';
|
|
11
11
|
export { configuredMcpRateLimit, loadCatenceConfig, resolvePaths } from '../core/runtime/configuration.js';
|
|
12
|
+
export { addAthlete, athleteStorePaths, defaultAthlete, defaultCatalogHome, initializeCatalog, loadCatalog, resolveAthlete, resolveCatalogPaths, } from '../core/runtime/catalog.js';
|
|
13
|
+
export type { Athlete, CatalogPaths, CatenceCatalog } from '../core/runtime/catalog.js';
|
|
14
|
+
export { athleteProviderEnvironment, providerSecretPath, readAthleteSecrets, setAthleteSecret } from '../core/runtime/secrets.js';
|
|
15
|
+
export type { AthleteSecrets, SecretProvider } from '../core/runtime/secrets.js';
|
|
12
16
|
export { SlidingWindowLimiter } from '../core/runtime/limiter.js';
|
|
13
17
|
export { DashboardSnapshotService } from '../core/dashboard/snapshot.js';
|
|
14
18
|
export { searchContext } from '../core/retrieval/index.js';
|