untappd-mcp 1.3.1 → 1.4.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -6
- package/SKILL.md +5 -5
- package/dist/bundle.js +236 -138
- package/dist/cache/db.js +36 -264
- package/dist/cache/store.js +333 -0
- package/dist/cache/sync.js +11 -11
- package/dist/index.js +12 -4
- package/dist/tools/cache.js +24 -30
- package/dist/tools/diagnostics.js +33 -0
- package/dist/tools/utilities.js +10 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cache/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHelpfulError, RateLimitError, messageOf } from '@chrischall/mcp-utils';
|
|
2
|
-
import { mapCheckinRow } from './
|
|
2
|
+
import { mapCheckinRow } from './store.js';
|
|
3
3
|
const PAGE_LIMIT = 50; // Untappd's max page size for /user/checkins.
|
|
4
4
|
/** One `/user/checkins` page fetch, tolerant of the fat/undocumented shape. */
|
|
5
5
|
async function fetchPage(client, encodedUser, maxId) {
|
|
@@ -46,7 +46,7 @@ async function fetchTotalCheckins(client, encodedUser) {
|
|
|
46
46
|
*/
|
|
47
47
|
export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
48
48
|
const encodedUser = encodeURIComponent(rawUsername);
|
|
49
|
-
const prior = cache.getState(rawUsername);
|
|
49
|
+
const prior = await cache.getState(rawUsername);
|
|
50
50
|
const priorNewest = prior?.newest_checkin_id ?? null;
|
|
51
51
|
let pages = 0;
|
|
52
52
|
let added = 0;
|
|
@@ -92,7 +92,7 @@ export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
|
92
92
|
break;
|
|
93
93
|
}
|
|
94
94
|
const rows = rowsOf(rawUsername, page.items);
|
|
95
|
-
added += cache.upsertCheckins(rawUsername, rows);
|
|
95
|
+
added += await cache.upsertCheckins(rawUsername, rows);
|
|
96
96
|
const oldestOnPage = rows[rows.length - 1].checkin_id;
|
|
97
97
|
if (oldestOnPage <= priorNewest) {
|
|
98
98
|
caughtUp = true; // reconnected to the cached block
|
|
@@ -108,16 +108,16 @@ export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
|
108
108
|
}
|
|
109
109
|
catchupMaxId = page.nextMaxId;
|
|
110
110
|
// Persist the resume cursor after EVERY page; the boundary stays put.
|
|
111
|
-
cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
|
|
111
|
+
await cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
|
|
112
112
|
maxId = page.nextMaxId;
|
|
113
113
|
}
|
|
114
114
|
if (caughtUp) {
|
|
115
115
|
// Contiguous from the true newest down to the old block: advance the
|
|
116
116
|
// boundary and clear the catch-up cursor.
|
|
117
|
-
newest = cache.newestCachedId(rawUsername);
|
|
117
|
+
newest = await cache.newestCachedId(rawUsername);
|
|
118
118
|
catchupMaxId = null;
|
|
119
119
|
catchupInProgress = false;
|
|
120
|
-
cache.setState(rawUsername, {
|
|
120
|
+
await cache.setState(rawUsername, {
|
|
121
121
|
newest_checkin_id: newest,
|
|
122
122
|
catchup_max_id: null,
|
|
123
123
|
backfill_complete: backfillComplete,
|
|
@@ -144,14 +144,14 @@ export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
|
144
144
|
break;
|
|
145
145
|
}
|
|
146
146
|
const rows = rowsOf(rawUsername, page.items);
|
|
147
|
-
added += cache.upsertCheckins(rawUsername, rows);
|
|
147
|
+
added += await cache.upsertCheckins(rawUsername, rows);
|
|
148
148
|
if (newest === null)
|
|
149
149
|
newest = rows[0].checkin_id; // first-ever sync sets the boundary
|
|
150
150
|
oldestMaxId = page.nextMaxId;
|
|
151
151
|
if (page.nextMaxId === null)
|
|
152
152
|
backfillComplete = true;
|
|
153
153
|
// Persist progress after EVERY page so an interruption never loses work.
|
|
154
|
-
cache.setState(rawUsername, {
|
|
154
|
+
await cache.setState(rawUsername, {
|
|
155
155
|
newest_checkin_id: newest,
|
|
156
156
|
oldest_max_id: oldestMaxId,
|
|
157
157
|
backfill_complete: backfillComplete,
|
|
@@ -163,8 +163,8 @@ export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
const total = await fetchTotalCheckins(client, encodedUser);
|
|
166
|
-
const cached = cache.cachedCount(rawUsername);
|
|
167
|
-
cache.setState(rawUsername, {
|
|
166
|
+
const cached = await cache.cachedCount(rawUsername);
|
|
167
|
+
await cache.setState(rawUsername, {
|
|
168
168
|
newest_checkin_id: newest,
|
|
169
169
|
oldest_max_id: oldestMaxId,
|
|
170
170
|
catchup_max_id: catchupMaxId,
|
|
@@ -190,7 +190,7 @@ export async function syncCheckins(client, cache, rawUsername, maxPages = 10) {
|
|
|
190
190
|
rows_added: added,
|
|
191
191
|
pages_fetched: pages,
|
|
192
192
|
cached_checkins: cached,
|
|
193
|
-
distinct_beers: cache.distinctBeerCount(rawUsername),
|
|
193
|
+
distinct_beers: await cache.distinctBeerCount(rawUsername),
|
|
194
194
|
total_checkins: total,
|
|
195
195
|
backfill_percent: backfillPercent,
|
|
196
196
|
backfill_complete: backfillComplete,
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,16 @@ import { registerWishlistTools } from './tools/wishlist.js';
|
|
|
14
14
|
import { registerCheckinTools } from './tools/checkin.js';
|
|
15
15
|
import { registerUtilityTools } from './tools/utilities.js';
|
|
16
16
|
import { registerCacheTools } from './tools/cache.js';
|
|
17
|
+
import { logRegisteredTools } from './tools/diagnostics.js';
|
|
18
|
+
import { CheckinCache, defaultCachePath } from './cache/db.js';
|
|
17
19
|
// Build the env-based client once and inject it into each registrar. The
|
|
18
20
|
// constructor defers its config error, so the server still boots (and answers
|
|
19
21
|
// the host's install-time tools/list probe) when credentials are absent.
|
|
20
22
|
const client = new UntappdClient();
|
|
23
|
+
// The stdio server backs the cache with a local `node:sqlite` file, opened lazily
|
|
24
|
+
// on first cache-tool use so the server still boots when no path is configured.
|
|
25
|
+
let nodeCache;
|
|
26
|
+
const nodeCacheProvider = () => (nodeCache ??= CheckinCache.open(defaultCachePath()));
|
|
21
27
|
await runMcp({
|
|
22
28
|
name: 'untappd-mcp',
|
|
23
29
|
version: VERSION,
|
|
@@ -34,9 +40,11 @@ await runMcp({
|
|
|
34
40
|
(s) => registerWishlistTools(s, client),
|
|
35
41
|
(s) => registerCheckinTools(s, client),
|
|
36
42
|
(s) => registerUtilityTools(s, client),
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
(s) => registerCacheTools(s, client),
|
|
43
|
+
// The stdio server backs the cache with a local `node:sqlite` file; the
|
|
44
|
+
// remote Cloudflare connector (src/worker.ts) backs the same cache tools
|
|
45
|
+
// with a per-user Durable Object instead.
|
|
46
|
+
(s) => registerCacheTools(s, client, nodeCacheProvider),
|
|
47
|
+
// Keep last: logs the full registered toolset (count + names) at startup.
|
|
48
|
+
(s) => logRegisteredTools(s, 'stdio'),
|
|
41
49
|
],
|
|
42
50
|
});
|
package/dist/tools/cache.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, toolAnnotations, createHelpfulError } from '@chrischall/mcp-utils';
|
|
3
|
-
import { CheckinCache, defaultCachePath } from '../cache/db.js';
|
|
4
3
|
import { syncCheckins } from '../cache/sync.js';
|
|
5
4
|
const UsernameArg = z
|
|
6
5
|
.string()
|
|
@@ -23,9 +22,9 @@ function resolveUser(username, loginName) {
|
|
|
23
22
|
* is, and a plain-language warning when a "not had" answer might be a false
|
|
24
23
|
* negative.
|
|
25
24
|
*/
|
|
26
|
-
function freshness(cache, username) {
|
|
27
|
-
const state = cache.getState(username);
|
|
28
|
-
const cached = cache.cachedCount(username);
|
|
25
|
+
async function freshness(cache, username) {
|
|
26
|
+
const state = await cache.getState(username);
|
|
27
|
+
const cached = await cache.cachedCount(username);
|
|
29
28
|
const backfillComplete = state?.backfill_complete ?? false;
|
|
30
29
|
const percent = state?.total_checkins && state.total_checkins > 0
|
|
31
30
|
? Math.min(100, Math.round((cached / state.total_checkins) * 100))
|
|
@@ -49,24 +48,24 @@ function freshness(cache, username) {
|
|
|
49
48
|
catchup_in_progress: catchupInProgress,
|
|
50
49
|
backfill_percent: percent,
|
|
51
50
|
cached_checkins: cached,
|
|
52
|
-
distinct_beers: cache.distinctBeerCount(username),
|
|
51
|
+
distinct_beers: await cache.distinctBeerCount(username),
|
|
53
52
|
...(caveat ? { caveat } : {}),
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
/**
|
|
57
|
-
* Register the
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* Register the check-in cache tools. `cacheProvider` supplies the backing store
|
|
57
|
+
* (a `node:sqlite` file on the stdio server, a Durable Object on the remote
|
|
58
|
+
* connector) and is required so this module imports no platform-specific code.
|
|
60
59
|
*/
|
|
61
|
-
export function registerCacheTools(server, client, cacheProvider
|
|
60
|
+
export function registerCacheTools(server, client, cacheProvider) {
|
|
62
61
|
server.registerTool('untappd_sync_checkins', {
|
|
63
|
-
title: 'Sync Untappd check-ins into the
|
|
64
|
-
description: "Fetch a user's check-ins into a
|
|
62
|
+
title: 'Sync Untappd check-ins into the cache',
|
|
63
|
+
description: "Fetch a user's check-ins into a persistent cache so history can be queried without paging the API. " +
|
|
65
64
|
'Incremental (stops once it reaches already-cached check-ins) and resumable: if the backfill is not yet ' +
|
|
66
65
|
'complete it pages backwards up to max_pages per call and persists progress after every page, so run it ' +
|
|
67
|
-
'repeatedly until
|
|
66
|
+
'repeatedly until another_run_needed is false. Syncing another user requires their account be public or a ' +
|
|
68
67
|
'friend. Omit username for your own account.',
|
|
69
|
-
annotations: toolAnnotations({ title: 'Sync Untappd check-ins into the
|
|
68
|
+
annotations: toolAnnotations({ title: 'Sync Untappd check-ins into the cache', readOnly: false, idempotent: false, openWorld: true }),
|
|
70
69
|
inputSchema: {
|
|
71
70
|
username: UsernameArg,
|
|
72
71
|
max_pages: z
|
|
@@ -83,12 +82,12 @@ export function registerCacheTools(server, client, cacheProvider = defaultCacheP
|
|
|
83
82
|
return textResult(summary);
|
|
84
83
|
});
|
|
85
84
|
server.registerTool('untappd_cache_has_had', {
|
|
86
|
-
title: 'Check if a user has had a beer (from
|
|
87
|
-
description: 'Answer "has this user ever checked in this beer?" from the
|
|
85
|
+
title: 'Check if a user has had a beer (from the cache)',
|
|
86
|
+
description: 'Answer "has this user ever checked in this beer?" from the cache only — NO API call. Match by exact ' +
|
|
88
87
|
'bid, or by a case-insensitive substring of the beer name. Returns whether they had it, how many times, the ' +
|
|
89
88
|
"best rating, the last date, and the matching check-ins. Always reports cache freshness so you can caveat " +
|
|
90
89
|
'incomplete data. Requires bid or beer_name. Run untappd_sync_checkins first.',
|
|
91
|
-
annotations: toolAnnotations({ title: 'Check if a user has had a beer (from
|
|
90
|
+
annotations: toolAnnotations({ title: 'Check if a user has had a beer (from the cache)', readOnly: true, idempotent: true, openWorld: false }),
|
|
92
91
|
inputSchema: {
|
|
93
92
|
username: UsernameArg,
|
|
94
93
|
bid: z.number().int().positive().optional().describe('Exact Untappd beer id to look for'),
|
|
@@ -100,15 +99,15 @@ export function registerCacheTools(server, client, cacheProvider = defaultCacheP
|
|
|
100
99
|
}
|
|
101
100
|
const user = resolveUser(username, client.loginName);
|
|
102
101
|
const cache = cacheProvider();
|
|
103
|
-
const result = cache.hasHad(user, { bid, beerName: beer_name });
|
|
104
|
-
return textResult({ username: user, query: { bid, beer_name }, ...result, freshness: freshness(cache, user) });
|
|
102
|
+
const result = await cache.hasHad(user, { bid, beerName: beer_name });
|
|
103
|
+
return textResult({ username: user, query: { bid, beer_name }, ...result, freshness: await freshness(cache, user) });
|
|
105
104
|
});
|
|
106
105
|
server.registerTool('untappd_cache_has_had_many', {
|
|
107
|
-
title: 'Batch-check many beers against the
|
|
106
|
+
title: 'Batch-check many beers against the cache',
|
|
108
107
|
description: 'Cross-check a list of beer ids against a user\'s cached history in ONE call — NO API call. Returns had/not-had ' +
|
|
109
108
|
'per bid (with count and last date when had). Ideal for checking a whole venue menu at once. Run ' +
|
|
110
109
|
'untappd_sync_checkins first; the freshness block flags if the cache is incomplete.',
|
|
111
|
-
annotations: toolAnnotations({ title: 'Batch-check many beers against the
|
|
110
|
+
annotations: toolAnnotations({ title: 'Batch-check many beers against the cache', readOnly: true, idempotent: true, openWorld: false }),
|
|
112
111
|
inputSchema: {
|
|
113
112
|
username: UsernameArg,
|
|
114
113
|
bids: z.array(z.number().int().positive()).min(1).max(500).describe('Beer ids to check (1–500)'),
|
|
@@ -124,7 +123,7 @@ export function registerCacheTools(server, client, cacheProvider = defaultCacheP
|
|
|
124
123
|
if (seen.has(bid))
|
|
125
124
|
continue;
|
|
126
125
|
seen.add(bid);
|
|
127
|
-
const r = cache.hasHad(user, { bid });
|
|
126
|
+
const r = await cache.hasHad(user, { bid });
|
|
128
127
|
if (r.had)
|
|
129
128
|
hadCount++;
|
|
130
129
|
results.push({ bid, had: r.had, count: r.count, last_date: r.last_date, best_rating: r.best_rating });
|
|
@@ -135,13 +134,13 @@ export function registerCacheTools(server, client, cacheProvider = defaultCacheP
|
|
|
135
134
|
had: hadCount,
|
|
136
135
|
not_had: seen.size - hadCount,
|
|
137
136
|
results,
|
|
138
|
-
freshness: freshness(cache, user),
|
|
137
|
+
freshness: await freshness(cache, user),
|
|
139
138
|
});
|
|
140
139
|
});
|
|
141
140
|
server.registerTool('untappd_cache_query', {
|
|
142
141
|
title: 'Query cached check-ins with filters',
|
|
143
142
|
description: "Query a user's cached check-ins by brewery, style, minimum rating, venue, and/or date range, with sorting " +
|
|
144
|
-
'and a limit — from the
|
|
143
|
+
'and a limit — from the cache only, NO API call. Returns the matching check-ins plus cache freshness. ' +
|
|
145
144
|
'Run untappd_sync_checkins first.',
|
|
146
145
|
annotations: toolAnnotations({ title: 'Query cached check-ins with filters', readOnly: true, idempotent: true, openWorld: false }),
|
|
147
146
|
inputSchema: {
|
|
@@ -163,12 +162,7 @@ export function registerCacheTools(server, client, cacheProvider = defaultCacheP
|
|
|
163
162
|
}, async ({ username, ...filters }) => {
|
|
164
163
|
const user = resolveUser(username, client.loginName);
|
|
165
164
|
const cache = cacheProvider();
|
|
166
|
-
const rows = cache.query(user, filters);
|
|
167
|
-
return textResult({ username: user, count: rows.length, filters, results: rows, freshness: freshness(cache, user) });
|
|
165
|
+
const rows = await cache.query(user, filters);
|
|
166
|
+
return textResult({ username: user, count: rows.length, filters, results: rows, freshness: await freshness(cache, user) });
|
|
168
167
|
});
|
|
169
168
|
}
|
|
170
|
-
/** Lazily open (once) the env-configured on-disk cache. */
|
|
171
|
-
function defaultCacheProvider() {
|
|
172
|
-
let cache;
|
|
173
|
-
return () => (cache ??= CheckinCache.open(defaultCachePath()));
|
|
174
|
-
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Small helpers so the live build's exact toolset is visible from the client
|
|
2
|
+
// (via untappd_healthcheck) and from server logs (a startup line). This is how
|
|
3
|
+
// you confirm a deploy actually shipped the tools you expect — e.g. that the
|
|
4
|
+
// remote connector is exposing the cache tools, not a stale 37-tool build.
|
|
5
|
+
/** The names of every tool registered on `server`, sorted. Defensive if the SDK internal shape drifts. */
|
|
6
|
+
export function registeredToolNames(server) {
|
|
7
|
+
const reg = server._registeredTools;
|
|
8
|
+
return reg ? Object.keys(reg).sort() : [];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* FNV-1a 32-bit hash of the sorted tool-name list — a stable, dependency-free
|
|
12
|
+
* fingerprint of the exposed toolset (works in both Node and Workers), so two
|
|
13
|
+
* builds can be compared at a glance without diffing the full list.
|
|
14
|
+
*/
|
|
15
|
+
export function toolsHash(names) {
|
|
16
|
+
let h = 0x811c9dc5;
|
|
17
|
+
const s = names.join(',');
|
|
18
|
+
for (let i = 0; i < s.length; i++) {
|
|
19
|
+
h ^= s.charCodeAt(i);
|
|
20
|
+
h = Math.imul(h, 0x01000193);
|
|
21
|
+
}
|
|
22
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
23
|
+
}
|
|
24
|
+
/** Version + tool count + tool-name-hash + names, for the healthcheck payload. */
|
|
25
|
+
export function toolInventory(server) {
|
|
26
|
+
const names = registeredToolNames(server);
|
|
27
|
+
return { tool_count: names.length, tools_hash: toolsHash(names), tools: names };
|
|
28
|
+
}
|
|
29
|
+
/** Emit a one-line startup log of the registered tools (stderr on Node, logs on Workers). */
|
|
30
|
+
export function logRegisteredTools(server, context) {
|
|
31
|
+
const names = registeredToolNames(server);
|
|
32
|
+
console.error(`[untappd-mcp] ${context}: ${names.length} tools registered [${toolsHash(names)}]: ${names.join(', ')}`);
|
|
33
|
+
}
|
package/dist/tools/utilities.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { textResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
|
+
import { VERSION } from '../version.js';
|
|
3
|
+
import { toolInventory } from './diagnostics.js';
|
|
2
4
|
export function registerUtilityTools(server, client) {
|
|
3
5
|
server.registerTool('untappd_healthcheck', {
|
|
4
6
|
title: 'Untappd healthcheck',
|
|
5
7
|
description: 'Verify Untappd connectivity and that credentials are configured and can log in. Performs a lightweight ' +
|
|
6
|
-
'authenticated request (your recent feed) and reports whether it succeeded
|
|
8
|
+
'authenticated request (your recent feed) and reports whether it succeeded, plus the running server version ' +
|
|
9
|
+
'and the exact set of tools this build exposes (count, a stable hash, and their names) so you can confirm ' +
|
|
10
|
+
'which build is live. Read-only.',
|
|
7
11
|
annotations: toolAnnotations({ title: 'Untappd healthcheck', readOnly: true, idempotent: true, openWorld: true }),
|
|
8
12
|
inputSchema: {},
|
|
9
13
|
}, async () => {
|
|
14
|
+
// Always report build/tool diagnostics, even when credentials are absent,
|
|
15
|
+
// so the live build and its toolset can be confirmed from the client.
|
|
16
|
+
const build = { server_version: VERSION, ...toolInventory(server) };
|
|
10
17
|
if (!client.configured) {
|
|
11
18
|
return textResult({
|
|
12
19
|
ok: false,
|
|
13
20
|
configured: false,
|
|
21
|
+
...build,
|
|
14
22
|
note: 'Untappd credentials are not set. Configure UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID, and UNTAPPD_CLIENT_SECRET.',
|
|
15
23
|
});
|
|
16
24
|
}
|
|
@@ -20,6 +28,7 @@ export function registerUtilityTools(server, client) {
|
|
|
20
28
|
configured: true,
|
|
21
29
|
account: client.loginName,
|
|
22
30
|
feed_reachable: feed?.checkins !== undefined,
|
|
31
|
+
...build,
|
|
23
32
|
note: 'Logged in to Untappd and fetched the friend feed successfully.',
|
|
24
33
|
});
|
|
25
34
|
});
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// json's `extra-files`), and `versionSyncTest` guards that it stays equal to
|
|
4
4
|
// package.json. Import VERSION wherever the version is needed rather than
|
|
5
5
|
// re-declaring it.
|
|
6
|
-
export const VERSION = '1.
|
|
6
|
+
export const VERSION = '1.4.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/untappd-mcp",
|
|
5
5
|
"description": "Untappd MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/untappd-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|