catence 0.0.0 → 0.2.0-beta.1

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.
Files changed (110) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +201 -29
  3. package/config.example.json +63 -0
  4. package/dist/contracts/index.d.ts +4 -0
  5. package/dist/contracts/index.js +5 -0
  6. package/dist/contracts/release.d.ts +20 -0
  7. package/dist/contracts/release.js +17 -0
  8. package/dist/contracts/runtime.d.ts +11 -0
  9. package/dist/contracts/runtime.js +2 -0
  10. package/dist/contracts/staging.d.ts +183 -0
  11. package/dist/contracts/staging.js +36 -0
  12. package/dist/contracts/storage.d.ts +15 -0
  13. package/dist/contracts/storage.js +2 -0
  14. package/dist/core/dashboard/snapshot.d.ts +16 -0
  15. package/dist/core/dashboard/snapshot.js +65 -0
  16. package/dist/core/query/activity-discovery.d.ts +21 -0
  17. package/dist/core/query/activity-discovery.js +133 -0
  18. package/dist/core/query/analytics.d.ts +59 -0
  19. package/dist/core/query/analytics.js +395 -0
  20. package/dist/core/query/catalog.d.ts +26 -0
  21. package/dist/core/query/catalog.js +141 -0
  22. package/dist/core/query/fitness.d.ts +40 -0
  23. package/dist/core/query/fitness.js +297 -0
  24. package/dist/core/query/repository.d.ts +19 -0
  25. package/dist/core/query/repository.js +182 -0
  26. package/dist/core/query/sql-guard.d.ts +7 -0
  27. package/dist/core/query/sql-guard.js +121 -0
  28. package/dist/core/query/sql.d.ts +2 -0
  29. package/dist/core/query/sql.js +5 -0
  30. package/dist/core/query/swimming.d.ts +19 -0
  31. package/dist/core/query/swimming.js +184 -0
  32. package/dist/core/query/wellness.d.ts +45 -0
  33. package/dist/core/query/wellness.js +292 -0
  34. package/dist/core/retrieval/index.d.ts +13 -0
  35. package/dist/core/retrieval/index.js +248 -0
  36. package/dist/core/runtime/catalog.d.ts +41 -0
  37. package/dist/core/runtime/catalog.js +111 -0
  38. package/dist/core/runtime/configuration.d.ts +71 -0
  39. package/dist/core/runtime/configuration.js +106 -0
  40. package/dist/core/runtime/limiter.d.ts +13 -0
  41. package/dist/core/runtime/limiter.js +17 -0
  42. package/dist/core/runtime/secrets.d.ts +24 -0
  43. package/dist/core/runtime/secrets.js +68 -0
  44. package/dist/elt/application/demo.d.ts +23 -0
  45. package/dist/elt/application/demo.js +165 -0
  46. package/dist/elt/application/management.d.ts +25 -0
  47. package/dist/elt/application/management.js +285 -0
  48. package/dist/elt/ingestion/importer.d.ts +8 -0
  49. package/dist/elt/ingestion/importer.js +87 -0
  50. package/dist/elt/ingestion/providers/intervals.d.ts +21 -0
  51. package/dist/elt/ingestion/providers/intervals.js +224 -0
  52. package/dist/elt/ingestion/providers/registry.d.ts +19 -0
  53. package/dist/elt/ingestion/providers/registry.js +62 -0
  54. package/dist/elt/ingestion/providers/strava/service.d.ts +42 -0
  55. package/dist/elt/ingestion/providers/strava/service.js +366 -0
  56. package/dist/elt/normalization/activities/linking.d.ts +5 -0
  57. package/dist/elt/normalization/activities/linking.js +165 -0
  58. package/dist/elt/normalization/normalizers.d.ts +5 -0
  59. package/dist/elt/normalization/normalizers.js +816 -0
  60. package/dist/elt/normalization/segments/strava-normalizer.d.ts +3 -0
  61. package/dist/elt/normalization/segments/strava-normalizer.js +93 -0
  62. package/dist/elt/storage/artifacts.d.ts +13 -0
  63. package/dist/elt/storage/artifacts.js +48 -0
  64. package/dist/elt/storage/database.d.ts +74 -0
  65. package/dist/elt/storage/database.js +296 -0
  66. package/dist/elt/storage/migrations.d.ts +3 -0
  67. package/dist/elt/storage/migrations.js +832 -0
  68. package/dist/elt/storage/migrations.legacy.d.ts +12 -0
  69. package/dist/elt/storage/migrations.legacy.js +332 -0
  70. package/dist/elt/storage/sql.d.ts +2 -0
  71. package/dist/elt/storage/sql.js +7 -0
  72. package/dist/elt/storage/write-lock.d.ts +6 -0
  73. package/dist/elt/storage/write-lock.js +28 -0
  74. package/dist/elt/streams.d.ts +24 -0
  75. package/dist/elt/streams.js +62 -0
  76. package/dist/interfaces/cli/commands.d.ts +2 -0
  77. package/dist/interfaces/cli/commands.js +166 -0
  78. package/dist/interfaces/cli/main.d.ts +2 -0
  79. package/dist/interfaces/cli/main.js +3 -0
  80. package/dist/interfaces/http/cli-options.d.ts +9 -0
  81. package/dist/interfaces/http/cli-options.js +81 -0
  82. package/dist/interfaces/http/server.d.ts +17 -0
  83. package/dist/interfaces/http/server.js +196 -0
  84. package/dist/interfaces/mcp/cli-options.d.ts +6 -0
  85. package/dist/interfaces/mcp/cli-options.js +36 -0
  86. package/dist/interfaces/mcp/main.d.ts +2 -0
  87. package/dist/interfaces/mcp/main.js +66 -0
  88. package/dist/interfaces/mcp/server.d.ts +8 -0
  89. package/dist/interfaces/mcp/server.js +572 -0
  90. package/dist/runtime/index.d.ts +33 -0
  91. package/dist/runtime/index.js +21 -0
  92. package/docs/architecture.md +100 -0
  93. package/docs/assets/image.png +0 -0
  94. package/docs/mcp-activity-retrieval.md +377 -0
  95. package/package.json +81 -6
  96. package/pyproject.toml +28 -0
  97. package/python/__init__.py +1 -0
  98. package/python/catence/__init__.py +0 -0
  99. package/python/catence/providers/__init__.py +0 -0
  100. package/python/catence/providers/garmin/__init__.py +3 -0
  101. package/python/catence/providers/garmin/cli.py +62 -0
  102. package/python/catence/providers/garmin/contracts.py +23 -0
  103. package/python/catence/providers/garmin/registry.py +48 -0
  104. package/python/catence/providers/garmin/staging.py +87 -0
  105. package/python/catence/providers/garmin/streams.py +195 -0
  106. package/python/catence/providers/garmin/worker.py +450 -0
  107. package/python/catence/providers/strava/__init__.py +3 -0
  108. package/python/catence/providers/strava/cli.py +404 -0
  109. package/python/catence/providers/strava/staging.py +73 -0
  110. package/uv.lock +708 -0
@@ -0,0 +1,9 @@
1
+ export type ServeCliOptions = {
2
+ home?: string;
3
+ host: string;
4
+ port: number;
5
+ allowedOrigins: string[];
6
+ help: boolean;
7
+ };
8
+ export declare const SERVE_USAGE = "Usage: catence serve [options]\n\nRun Catence as a local Streamable HTTP MCP server.\n\nOptions:\n --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.\n --host <host> Listener host. Defaults to 127.0.0.1.\n --port <port> Listener port. Defaults to 8787.\n --allow-origin <origin> Browser origin allowed to call Catence APIs. May be repeated.\n -h, --help Show this help text.";
9
+ export declare function parseServeCliOptions(arguments_: readonly string[]): ServeCliOptions;
@@ -0,0 +1,81 @@
1
+ export const SERVE_USAGE = `Usage: catence serve [options]
2
+
3
+ Run Catence as a local Streamable HTTP MCP server.
4
+
5
+ Options:
6
+ --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.
7
+ --host <host> Listener host. Defaults to 127.0.0.1.
8
+ --port <port> Listener port. Defaults to 8787.
9
+ --allow-origin <origin> Browser origin allowed to call Catence APIs. May be repeated.
10
+ -h, --help Show this help text.`;
11
+ function requiredValue(arguments_, index, option) {
12
+ const value = arguments_[index + 1];
13
+ if (!value || value.startsWith('-'))
14
+ throw new Error(`${option} requires a value.`);
15
+ return value;
16
+ }
17
+ function parsePort(value) {
18
+ if (!/^\d+$/.test(value))
19
+ throw new Error('--port must be an integer between 1 and 65535.');
20
+ const port = Number(value);
21
+ if (port < 1 || port > 65_535)
22
+ throw new Error('--port must be an integer between 1 and 65535.');
23
+ return port;
24
+ }
25
+ export function parseServeCliOptions(arguments_) {
26
+ let home;
27
+ let host = process.env.CATENCE_HTTP_HOST ?? '127.0.0.1';
28
+ let port = process.env.CATENCE_HTTP_PORT ? parsePort(process.env.CATENCE_HTTP_PORT) : 8787;
29
+ const allowedOrigins = [];
30
+ for (let index = 0; index < arguments_.length; index++) {
31
+ const argument = arguments_[index];
32
+ if (argument === '-h' || argument === '--help')
33
+ return { home, host, port, allowedOrigins, help: true };
34
+ if (argument === '--home') {
35
+ home = requiredValue(arguments_, index, '--home');
36
+ index++;
37
+ continue;
38
+ }
39
+ if (argument.startsWith('--home=')) {
40
+ home = argument.slice('--home='.length);
41
+ if (!home)
42
+ throw new Error('--home requires a value.');
43
+ continue;
44
+ }
45
+ if (argument === '--host') {
46
+ host = requiredValue(arguments_, index, '--host');
47
+ index++;
48
+ continue;
49
+ }
50
+ if (argument.startsWith('--host=')) {
51
+ host = argument.slice('--host='.length);
52
+ if (!host)
53
+ throw new Error('--host requires a value.');
54
+ continue;
55
+ }
56
+ if (argument === '--port') {
57
+ port = parsePort(requiredValue(arguments_, index, '--port'));
58
+ index++;
59
+ continue;
60
+ }
61
+ if (argument.startsWith('--port=')) {
62
+ port = parsePort(argument.slice('--port='.length));
63
+ continue;
64
+ }
65
+ if (argument === '--allow-origin') {
66
+ allowedOrigins.push(requiredValue(arguments_, index, '--allow-origin'));
67
+ index++;
68
+ continue;
69
+ }
70
+ if (argument.startsWith('--allow-origin=')) {
71
+ const origin = argument.slice('--allow-origin='.length);
72
+ if (!origin)
73
+ throw new Error('--allow-origin requires a value.');
74
+ allowedOrigins.push(origin);
75
+ continue;
76
+ }
77
+ throw new Error(`Unknown option: ${argument}`);
78
+ }
79
+ return { home, host, port, allowedOrigins, help: false };
80
+ }
81
+ //# sourceMappingURL=cli-options.js.map
@@ -0,0 +1,17 @@
1
+ import { type Server } from 'node:http';
2
+ import { type CatalogPaths, type CatencePaths } from '../../runtime/index.js';
3
+ export type CatenceHttpServerOptions = {
4
+ /** A shared catalog for public servers. All data APIs then require athleteId. */
5
+ catalogPaths?: CatalogPaths;
6
+ /** Internal compatibility hook for tests and embedded single-store callers. */
7
+ paths?: CatencePaths;
8
+ allowedOrigins?: readonly string[];
9
+ };
10
+ /**
11
+ * Create Catence's local Streamable HTTP server.
12
+ *
13
+ * Each MCP request gets an independent stateless transport and server instance.
14
+ * This keeps the HTTP surface compatible with local clients without introducing
15
+ * process-wide MCP session state or a second query implementation.
16
+ */
17
+ export declare function createCatenceHttpServer(options?: CatenceHttpServerOptions): Server;
@@ -0,0 +1,196 @@
1
+ import { createServer } from 'node:http';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { catenceRuntimeHealth, DashboardSnapshotService, jsonSafe, loadCatalog, openReadOnlyRepository, resolveAthlete, resolveCatalogPaths } from '../../runtime/index.js';
4
+ import { createCatenceMcpServer } from '../mcp/server.js';
5
+ const MAX_REQUEST_BYTES = 1_000_000;
6
+ function json(response, statusCode, value) {
7
+ const body = JSON.stringify(jsonSafe(value));
8
+ response.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' });
9
+ response.end(body);
10
+ }
11
+ function addCorsHeaders(request, response, allowedOrigins) {
12
+ const origin = request.headers.origin;
13
+ if (!origin)
14
+ return true;
15
+ if (!allowedOrigins.includes(origin))
16
+ return false;
17
+ response.setHeader('access-control-allow-origin', origin);
18
+ response.setHeader('vary', 'Origin');
19
+ response.setHeader('access-control-allow-methods', 'GET, POST, DELETE, OPTIONS');
20
+ response.setHeader('access-control-allow-headers', 'content-type, mcp-session-id');
21
+ return true;
22
+ }
23
+ async function readJsonBody(request) {
24
+ let size = 0;
25
+ const chunks = [];
26
+ for await (const chunk of request) {
27
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
+ size += buffer.length;
29
+ if (size > MAX_REQUEST_BYTES)
30
+ throw new Error(`Request body exceeds the ${MAX_REQUEST_BYTES} byte limit.`);
31
+ chunks.push(buffer);
32
+ }
33
+ const body = Buffer.concat(chunks).toString('utf8').trim();
34
+ if (!body)
35
+ return undefined;
36
+ try {
37
+ return JSON.parse(body);
38
+ }
39
+ catch {
40
+ throw new Error('Request body must be valid JSON.');
41
+ }
42
+ }
43
+ function methodNotAllowed(response) {
44
+ json(response, 405, {
45
+ jsonrpc: '2.0',
46
+ error: { code: -32000, message: 'Method not allowed.' },
47
+ id: null,
48
+ });
49
+ }
50
+ function parseDate(value, name) {
51
+ if (value === null)
52
+ return undefined;
53
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00.000Z`))) {
54
+ throw new Error(`${name} must use YYYY-MM-DD.`);
55
+ }
56
+ return value;
57
+ }
58
+ function dashboardOptions(url) {
59
+ const endDate = parseDate(url.searchParams.get('endDate'), 'endDate') ?? new Date().toISOString().slice(0, 10);
60
+ const daysValue = url.searchParams.get('days') ?? '28';
61
+ if (!/^\d+$/.test(daysValue))
62
+ throw new Error('days must be an integer between 1 and 90.');
63
+ const days = Number(daysValue);
64
+ if (days < 1 || days > 90)
65
+ throw new Error('days must be an integer between 1 and 90.');
66
+ return { endDate, days };
67
+ }
68
+ async function dashboardSnapshot(paths, url) {
69
+ const repository = await openReadOnlyRepository(paths);
70
+ try {
71
+ return await new DashboardSnapshotService(repository).snapshot(dashboardOptions(url));
72
+ }
73
+ finally {
74
+ await repository.close();
75
+ }
76
+ }
77
+ async function dashboardPaths(catalogPaths, staticPaths, url) {
78
+ if (catalogPaths) {
79
+ const athleteId = url.searchParams.get('athleteId');
80
+ if (!athleteId)
81
+ throw new Error('athleteId is required. Call /api/v1/athletes to inspect the configured catalog.');
82
+ return (await resolveAthlete(catalogPaths, athleteId)).paths;
83
+ }
84
+ if (staticPaths)
85
+ return staticPaths;
86
+ throw new Error('No Catence data store is configured.');
87
+ }
88
+ async function handleMcpRequest(request, response, paths) {
89
+ if (request.method !== 'POST') {
90
+ methodNotAllowed(response);
91
+ return;
92
+ }
93
+ let body;
94
+ try {
95
+ body = await readJsonBody(request);
96
+ }
97
+ catch (error) {
98
+ json(response, 400, {
99
+ jsonrpc: '2.0',
100
+ error: { code: -32700, message: error instanceof Error ? error.message : String(error) },
101
+ id: null,
102
+ });
103
+ return;
104
+ }
105
+ const server = createCatenceMcpServer(paths);
106
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
107
+ let cleanedUp = false;
108
+ const cleanup = async () => {
109
+ if (cleanedUp)
110
+ return;
111
+ cleanedUp = true;
112
+ await Promise.allSettled([transport.close(), server.close()]);
113
+ };
114
+ response.once('close', () => { void cleanup(); });
115
+ try {
116
+ await server.connect(transport);
117
+ await transport.handleRequest(request, response, body);
118
+ }
119
+ catch (error) {
120
+ if (!response.headersSent) {
121
+ json(response, 500, {
122
+ jsonrpc: '2.0',
123
+ error: { code: -32603, message: error instanceof Error ? error.message : 'Internal server error.' },
124
+ id: null,
125
+ });
126
+ }
127
+ }
128
+ finally {
129
+ // The response may already have ended before handleRequest resolves, in
130
+ // which case adding a close listener here would miss cleanup entirely.
131
+ if (response.writableEnded || response.destroyed)
132
+ void cleanup();
133
+ }
134
+ }
135
+ /**
136
+ * Create Catence's local Streamable HTTP server.
137
+ *
138
+ * Each MCP request gets an independent stateless transport and server instance.
139
+ * This keeps the HTTP surface compatible with local clients without introducing
140
+ * process-wide MCP session state or a second query implementation.
141
+ */
142
+ export function createCatenceHttpServer(options = {}) {
143
+ const catalogPaths = options.catalogPaths ?? (options.paths ? null : resolveCatalogPaths());
144
+ const staticPaths = options.paths ?? null;
145
+ const allowedOrigins = options.allowedOrigins ?? [];
146
+ return createServer((request, response) => {
147
+ void (async () => {
148
+ if (!addCorsHeaders(request, response, allowedOrigins)) {
149
+ json(response, 403, { error: { code: 'origin_not_allowed', message: 'This browser origin is not allowed to access Catence.' } });
150
+ return;
151
+ }
152
+ const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
153
+ if (request.method === 'OPTIONS') {
154
+ response.writeHead(204).end();
155
+ return;
156
+ }
157
+ if (pathname === '/health' && request.method === 'GET') {
158
+ json(response, 200, catenceRuntimeHealth());
159
+ return;
160
+ }
161
+ if (pathname === '/api/v1/athletes' && request.method === 'GET') {
162
+ if (!catalogPaths) {
163
+ json(response, 200, { defaultAthleteId: 'local', athletes: [{ id: 'local', label: 'Local athlete' }] });
164
+ return;
165
+ }
166
+ try {
167
+ const catalog = await loadCatalog(catalogPaths);
168
+ json(response, 200, { defaultAthleteId: catalog.defaultAthleteId, athletes: catalog.athletes });
169
+ }
170
+ catch (error) {
171
+ json(response, 400, { error: { code: 'catalog_unavailable', message: error instanceof Error ? error.message : String(error) } });
172
+ }
173
+ return;
174
+ }
175
+ if (pathname === '/api/v1/dashboard' && request.method === 'GET') {
176
+ try {
177
+ const url = new URL(request.url ?? '/', 'http://localhost');
178
+ json(response, 200, await dashboardSnapshot(await dashboardPaths(catalogPaths, staticPaths, url), url));
179
+ }
180
+ catch (error) {
181
+ json(response, 400, { error: { code: 'dashboard_unavailable', message: error instanceof Error ? error.message : String(error) } });
182
+ }
183
+ return;
184
+ }
185
+ if (pathname === '/mcp') {
186
+ await handleMcpRequest(request, response, catalogPaths ?? staticPaths ?? resolveCatalogPaths());
187
+ return;
188
+ }
189
+ json(response, 404, { error: { code: 'not_found', message: 'No Catence endpoint matches this request.' } });
190
+ })().catch((error) => {
191
+ if (!response.headersSent)
192
+ json(response, 500, { error: { code: 'internal_error', message: error instanceof Error ? error.message : String(error) } });
193
+ });
194
+ });
195
+ }
196
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,6 @@
1
+ export type McpCliOptions = {
2
+ home?: string;
3
+ help: boolean;
4
+ };
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
+ export declare function parseMcpCliOptions(arguments_: readonly string[]): McpCliOptions;
@@ -0,0 +1,36 @@
1
+ export const MCP_USAGE = `Usage: catence [--home <directory>]
2
+ catence demo [--home <directory>]
3
+
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
+
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
+
8
+ Options:
9
+ --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.
10
+ -h, --help Show this help text.`;
11
+ export function parseMcpCliOptions(arguments_) {
12
+ let home;
13
+ for (let index = 0; index < arguments_.length; index++) {
14
+ const argument = arguments_[index];
15
+ if (argument === '-h' || argument === '--help')
16
+ return { home, help: true };
17
+ if (argument === '--home') {
18
+ const value = arguments_[index + 1];
19
+ if (!value || value.startsWith('-'))
20
+ throw new Error('--home requires a directory.');
21
+ home = value;
22
+ index++;
23
+ continue;
24
+ }
25
+ if (argument.startsWith('--home=')) {
26
+ const value = argument.slice('--home='.length);
27
+ if (!value)
28
+ throw new Error('--home requires a directory.');
29
+ home = value;
30
+ continue;
31
+ }
32
+ throw new Error(`Unknown option: ${argument}`);
33
+ }
34
+ return { home, help: false };
35
+ }
36
+ //# sourceMappingURL=cli-options.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { existsSync } from 'node:fs';
4
+ import { addAthlete, athleteStorePaths, createDemoStore, defaultCatalogHome, initializeCatalog, loadCatalog, resolveCatalogPaths } from '../../runtime/index.js';
5
+ import { parseServeCliOptions, SERVE_USAGE } from '../http/cli-options.js';
6
+ import { createCatenceHttpServer } from '../http/server.js';
7
+ import { MCP_USAGE, parseMcpCliOptions } from './cli-options.js';
8
+ import { createCatenceMcpServer } from './server.js';
9
+ async function run() {
10
+ const arguments_ = process.argv.slice(2);
11
+ if (arguments_[0] === 'serve') {
12
+ const options = parseServeCliOptions(arguments_.slice(1));
13
+ if (options.help) {
14
+ process.stderr.write(`${SERVE_USAGE}\n`);
15
+ return;
16
+ }
17
+ const httpServer = createCatenceHttpServer({
18
+ catalogPaths: resolveCatalogPaths(options.home),
19
+ allowedOrigins: options.allowedOrigins,
20
+ });
21
+ await new Promise((resolve, reject) => {
22
+ httpServer.once('error', reject);
23
+ httpServer.listen(options.port, options.host, () => {
24
+ httpServer.off('error', reject);
25
+ resolve();
26
+ });
27
+ });
28
+ console.error(`Catence Streamable HTTP server is listening at http://${options.host}:${options.port}/mcp`);
29
+ const close = () => {
30
+ httpServer.close(() => process.exit(0));
31
+ };
32
+ process.once('SIGINT', close);
33
+ process.once('SIGTERM', close);
34
+ return;
35
+ }
36
+ const demo = arguments_[0] === 'demo';
37
+ const options = parseMcpCliOptions(demo ? arguments_.slice(1) : arguments_);
38
+ if (options.help) {
39
+ process.stderr.write(`${MCP_USAGE}\n`);
40
+ return;
41
+ }
42
+ const catalogPaths = resolveCatalogPaths(demo && !options.home ? `${defaultCatalogHome()}-demo` : options.home);
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');
51
+ const result = await createDemoStore(paths);
52
+ process.stderr.write(`${String(result.message)}\n`);
53
+ }
54
+ const server = createCatenceMcpServer(catalogPaths);
55
+ await server.connect(new StdioServerTransport());
56
+ console.error('Catence MCP server is running on stdio; ordinary tools are read-only and Strava hydration is lock-guarded.');
57
+ }
58
+ try {
59
+ await run();
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ process.stderr.write(`catence: ${message}\n\n${process.argv[2] === 'serve' ? SERVE_USAGE : MCP_USAGE}\n`);
64
+ process.exitCode = 2;
65
+ }
66
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,8 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type CatalogPaths, hydrateStravaActivity, type CatencePaths } from '../../runtime/index.js';
3
+ type StravaActivityHydrator = typeof hydrateStravaActivity;
4
+ type McpDependencies = {
5
+ hydrateStravaActivity?: StravaActivityHydrator;
6
+ };
7
+ export declare function createCatenceMcpServer(paths?: CatencePaths | CatalogPaths, dependencies?: McpDependencies): McpServer;
8
+ export {};