anyapi-mcp-server 1.8.1 → 2.0.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/README.md +3 -2
- package/build/data-cache.js +5 -1
- package/build/error-context.js +1 -1
- package/build/graphql-schema.js +61 -6
- package/build/index.js +13 -699
- package/build/json-patch.js +137 -0
- package/build/oauth.js +0 -3
- package/build/pagination.js +1 -1
- package/build/pre-write-backup.js +19 -1
- package/build/token-budget.js +88 -45
- package/build/tools/auth.js +117 -0
- package/build/tools/call-api.js +204 -0
- package/build/tools/explain-api.js +86 -0
- package/build/tools/inspect-api.js +213 -0
- package/build/tools/list-api.js +82 -0
- package/build/tools/mutate-api.js +222 -0
- package/build/tools/query-api.js +183 -0
- package/build/tools/shared.js +65 -0
- package/build/write-safety.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6902 JSON Patch subset: add, remove, replace operations.
|
|
3
|
+
* Used by mutate_api to apply targeted changes to large resources
|
|
4
|
+
* without requiring the LLM to hold the full state in context.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Parse a JSON Pointer (RFC 6901) into path segments.
|
|
8
|
+
* Unescapes ~1 → / and ~0 → ~ per spec.
|
|
9
|
+
*/
|
|
10
|
+
function parsePointer(path) {
|
|
11
|
+
if (path === "")
|
|
12
|
+
return [];
|
|
13
|
+
if (!path.startsWith("/")) {
|
|
14
|
+
throw new Error(`Invalid JSON Pointer: must start with '/' (got '${path}')`);
|
|
15
|
+
}
|
|
16
|
+
return path
|
|
17
|
+
.slice(1)
|
|
18
|
+
.split("/")
|
|
19
|
+
.map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Walk to the parent of the target path segment.
|
|
23
|
+
* Returns [parent, lastSegment] for the operation to act on.
|
|
24
|
+
*/
|
|
25
|
+
function walkToParent(root, segments) {
|
|
26
|
+
let current = root;
|
|
27
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
28
|
+
const seg = segments[i];
|
|
29
|
+
if (Array.isArray(current)) {
|
|
30
|
+
const idx = parseInt(seg, 10);
|
|
31
|
+
if (isNaN(idx) || idx < 0 || idx >= current.length) {
|
|
32
|
+
throw new Error(`Array index out of bounds: '${seg}' at /${segments.slice(0, i + 1).join("/")}`);
|
|
33
|
+
}
|
|
34
|
+
current = current[idx];
|
|
35
|
+
}
|
|
36
|
+
else if (typeof current === "object" && current !== null) {
|
|
37
|
+
const rec = current;
|
|
38
|
+
if (!(seg in rec)) {
|
|
39
|
+
throw new Error(`Path not found: '${seg}' at /${segments.slice(0, i + 1).join("/")}`);
|
|
40
|
+
}
|
|
41
|
+
current = rec[seg];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
throw new Error(`Cannot traverse into ${typeof current} at /${segments.slice(0, i + 1).join("/")}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(current) && (typeof current !== "object" || current === null)) {
|
|
48
|
+
throw new Error(`Cannot apply operation: parent at /${segments.slice(0, -1).join("/")} is ${typeof current}`);
|
|
49
|
+
}
|
|
50
|
+
return [current, segments[segments.length - 1]];
|
|
51
|
+
}
|
|
52
|
+
function applyAdd(root, segments, value) {
|
|
53
|
+
const [parent, key] = walkToParent(root, segments);
|
|
54
|
+
if (Array.isArray(parent)) {
|
|
55
|
+
if (key === "-") {
|
|
56
|
+
parent.push(value);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const idx = parseInt(key, 10);
|
|
60
|
+
if (isNaN(idx) || idx < 0 || idx > parent.length) {
|
|
61
|
+
throw new Error(`Array index out of bounds for add: '${key}'`);
|
|
62
|
+
}
|
|
63
|
+
parent.splice(idx, 0, value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
parent[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function applyRemove(root, segments) {
|
|
71
|
+
const [parent, key] = walkToParent(root, segments);
|
|
72
|
+
if (Array.isArray(parent)) {
|
|
73
|
+
const idx = parseInt(key, 10);
|
|
74
|
+
if (isNaN(idx) || idx < 0 || idx >= parent.length) {
|
|
75
|
+
throw new Error(`Array index out of bounds for remove: '${key}'`);
|
|
76
|
+
}
|
|
77
|
+
parent.splice(idx, 1);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const rec = parent;
|
|
81
|
+
if (!(key in rec)) {
|
|
82
|
+
throw new Error(`Cannot remove non-existent key: '${key}'`);
|
|
83
|
+
}
|
|
84
|
+
delete rec[key];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function applyReplace(root, segments, value) {
|
|
88
|
+
const [parent, key] = walkToParent(root, segments);
|
|
89
|
+
if (Array.isArray(parent)) {
|
|
90
|
+
const idx = parseInt(key, 10);
|
|
91
|
+
if (isNaN(idx) || idx < 0 || idx >= parent.length) {
|
|
92
|
+
throw new Error(`Array index out of bounds for replace: '${key}'`);
|
|
93
|
+
}
|
|
94
|
+
parent[idx] = value;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const rec = parent;
|
|
98
|
+
if (!(key in rec)) {
|
|
99
|
+
throw new Error(`Cannot replace non-existent key: '${key}'`);
|
|
100
|
+
}
|
|
101
|
+
rec[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Apply a sequence of JSON Patch operations to a deep-cloned copy of the target.
|
|
106
|
+
* Returns the patched object. Does not mutate the original.
|
|
107
|
+
*/
|
|
108
|
+
export function applyPatch(target, operations) {
|
|
109
|
+
const cloned = JSON.parse(JSON.stringify(target));
|
|
110
|
+
for (let i = 0; i < operations.length; i++) {
|
|
111
|
+
const op = operations[i];
|
|
112
|
+
const segments = parsePointer(op.path);
|
|
113
|
+
if (segments.length === 0) {
|
|
114
|
+
throw new Error(`Operation ${i} (${op.op}): cannot target root`);
|
|
115
|
+
}
|
|
116
|
+
switch (op.op) {
|
|
117
|
+
case "add":
|
|
118
|
+
if (op.value === undefined) {
|
|
119
|
+
throw new Error(`Operation ${i} (add): 'value' is required`);
|
|
120
|
+
}
|
|
121
|
+
applyAdd(cloned, segments, op.value);
|
|
122
|
+
break;
|
|
123
|
+
case "remove":
|
|
124
|
+
applyRemove(cloned, segments);
|
|
125
|
+
break;
|
|
126
|
+
case "replace":
|
|
127
|
+
if (op.value === undefined) {
|
|
128
|
+
throw new Error(`Operation ${i} (replace): 'value' is required`);
|
|
129
|
+
}
|
|
130
|
+
applyReplace(cloned, segments, op.value);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Operation ${i}: unsupported op '${op.op}'`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return cloned;
|
|
137
|
+
}
|
package/build/oauth.js
CHANGED
package/build/pagination.js
CHANGED
|
@@ -28,7 +28,7 @@ function walkPath(data, path) {
|
|
|
28
28
|
return current;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Known pagination-related query parameter names (used to flag params in
|
|
31
|
+
* Known pagination-related query parameter names (used to flag params in inspect_api).
|
|
32
32
|
*/
|
|
33
33
|
export const PAGINATION_PARAM_NAMES = new Set([
|
|
34
34
|
"page", "cursor", "after", "before", "limit", "offset", "per_page",
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { callApi } from "./api-client.js";
|
|
2
2
|
import { storeResponse } from "./data-cache.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extract parameter names from a path template (e.g., "/items/{id}" → Set(["id"])).
|
|
5
|
+
*/
|
|
6
|
+
export function extractPathParamNames(pathTemplate) {
|
|
7
|
+
const names = new Set();
|
|
8
|
+
pathTemplate.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
9
|
+
names.add(name);
|
|
10
|
+
return "";
|
|
11
|
+
});
|
|
12
|
+
return names;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Create a pre-write backup by fetching the current state of a resource via GET.
|
|
16
|
+
* Only forwards path parameters — query params are stripped to avoid fetching
|
|
17
|
+
* a filtered/paginated subset of the resource.
|
|
5
18
|
* Returns a dataKey for the cached snapshot, or undefined on failure.
|
|
6
19
|
* Failure is non-fatal — errors are logged to stderr.
|
|
7
20
|
*/
|
|
@@ -9,7 +22,12 @@ export async function createBackup(config, method, path, params, headers) {
|
|
|
9
22
|
if (method !== "PATCH" && method !== "PUT")
|
|
10
23
|
return undefined;
|
|
11
24
|
try {
|
|
12
|
-
|
|
25
|
+
// Only forward path parameters to avoid fetching a filtered/paginated response
|
|
26
|
+
const pathParamNames = extractPathParamNames(path);
|
|
27
|
+
const pathOnlyParams = params
|
|
28
|
+
? Object.fromEntries(Object.entries(params).filter(([k]) => pathParamNames.has(k)))
|
|
29
|
+
: undefined;
|
|
30
|
+
const { data, responseHeaders } = await callApi(config, "GET", path, pathOnlyParams && Object.keys(pathOnlyParams).length > 0 ? pathOnlyParams : undefined, undefined, headers);
|
|
13
31
|
return storeResponse("GET", path, data, responseHeaders);
|
|
14
32
|
}
|
|
15
33
|
catch (err) {
|
package/build/token-budget.js
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* Truncates array results to fit within a token budget,
|
|
4
4
|
* leveraging GraphQL field selection for size control.
|
|
5
5
|
*/
|
|
6
|
-
const DEFAULT_BUDGET = 4000;
|
|
7
6
|
/**
|
|
8
7
|
* Find the primary array in a query result.
|
|
9
8
|
* Checks `items` first (raw array responses), then falls back to
|
|
10
9
|
* the first non-`_`-prefixed array field.
|
|
11
10
|
*/
|
|
12
|
-
|
|
11
|
+
function findPrimaryArray(obj) {
|
|
13
12
|
if (typeof obj !== "object" || obj === null || Array.isArray(obj))
|
|
14
13
|
return null;
|
|
15
14
|
const rec = obj;
|
|
@@ -44,40 +43,78 @@ function estimateTokens(value) {
|
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
48
|
-
* Uses binary search to find the max number of items that fit.
|
|
49
|
-
* Returns the truncated object and the original/truncated counts.
|
|
46
|
+
* Public re-export of estimateTokens for use in index.ts.
|
|
50
47
|
*/
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
export function estimateResultTokens(value) {
|
|
49
|
+
return estimateTokens(value);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* BFS walk of the result tree to find the deepest, largest array by token cost.
|
|
53
|
+
* Skips `_`-prefixed keys. Returns null if no arrays found.
|
|
54
|
+
*/
|
|
55
|
+
export function findDeepestLargestArray(obj) {
|
|
56
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj))
|
|
57
|
+
return null;
|
|
58
|
+
let best = null;
|
|
59
|
+
// BFS queue: each entry is [currentObject, pathSoFar]
|
|
60
|
+
const queue = [
|
|
61
|
+
[obj, []],
|
|
62
|
+
];
|
|
63
|
+
while (queue.length > 0) {
|
|
64
|
+
const [current, currentPath] = queue.shift();
|
|
65
|
+
for (const [key, value] of Object.entries(current)) {
|
|
66
|
+
if (key.startsWith("_"))
|
|
67
|
+
continue;
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
const cost = estimateTokens(value);
|
|
70
|
+
if (!best ||
|
|
71
|
+
currentPath.length + 1 > best.path.length ||
|
|
72
|
+
(currentPath.length + 1 === best.path.length && cost > best.tokenCost)) {
|
|
73
|
+
best = { path: [...currentPath, key], array: value, tokenCost: cost };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
77
|
+
queue.push([value, [...currentPath, key]]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return best;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Truncate the deepest largest array in an object to fit within a token budget.
|
|
85
|
+
* Clones the spine of the path to avoid mutating the original.
|
|
86
|
+
* Returns the truncated object and original/kept counts.
|
|
87
|
+
*/
|
|
88
|
+
export function truncateDeepArray(obj, budget) {
|
|
89
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
54
90
|
return { result: obj, originalCount: 0, keptCount: 0 };
|
|
55
91
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
92
|
+
const target = findDeepestLargestArray(obj);
|
|
93
|
+
if (!target || target.array.length === 0) {
|
|
94
|
+
return { result: obj, originalCount: 0, keptCount: 0 };
|
|
95
|
+
}
|
|
96
|
+
const originalCount = target.array.length;
|
|
97
|
+
// Check if everything fits as-is
|
|
98
|
+
const totalTokens = estimateTokens(obj);
|
|
99
|
+
if (totalTokens <= budget) {
|
|
61
100
|
return { result: obj, originalCount, keptCount: originalCount };
|
|
62
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
63
|
-
if (key !== arrayKey) {
|
|
64
|
-
overhead[key] = value;
|
|
65
|
-
}
|
|
66
101
|
}
|
|
67
|
-
|
|
102
|
+
// Compute overhead: tokens of everything except the target array
|
|
103
|
+
// Build a copy with the target array emptied to measure overhead
|
|
104
|
+
const emptyClone = cloneSpine(obj, target.path, []);
|
|
105
|
+
const overheadTokens = estimateTokens(emptyClone);
|
|
68
106
|
const arrayBudget = Math.max(1, budget - overheadTokens);
|
|
69
|
-
// Check if full array fits
|
|
70
|
-
|
|
71
|
-
if (fullTokens <= arrayBudget) {
|
|
107
|
+
// Check if full array fits within array budget
|
|
108
|
+
if (target.tokenCost <= arrayBudget) {
|
|
72
109
|
return { result: obj, originalCount, keptCount: originalCount };
|
|
73
110
|
}
|
|
74
111
|
// Binary search for max items that fit
|
|
75
112
|
let lo = 1;
|
|
76
113
|
let hi = originalCount;
|
|
77
|
-
let bestCount = 1;
|
|
114
|
+
let bestCount = 1;
|
|
78
115
|
while (lo <= hi) {
|
|
79
116
|
const mid = Math.floor((lo + hi) / 2);
|
|
80
|
-
const sliceTokens = estimateTokens(
|
|
117
|
+
const sliceTokens = estimateTokens(target.array.slice(0, mid));
|
|
81
118
|
if (sliceTokens <= arrayBudget) {
|
|
82
119
|
bestCount = mid;
|
|
83
120
|
lo = mid + 1;
|
|
@@ -86,50 +123,56 @@ export function truncateToTokenBudget(obj, budget) {
|
|
|
86
123
|
hi = mid - 1;
|
|
87
124
|
}
|
|
88
125
|
}
|
|
89
|
-
const
|
|
90
|
-
return { result:
|
|
126
|
+
const truncatedResult = cloneSpine(obj, target.path, target.array.slice(0, bestCount));
|
|
127
|
+
return { result: truncatedResult, originalCount, keptCount: bestCount };
|
|
91
128
|
}
|
|
92
129
|
/**
|
|
93
|
-
*
|
|
130
|
+
* Clone the spine of an object along a path, replacing the leaf with a new value.
|
|
94
131
|
*/
|
|
95
|
-
function
|
|
96
|
-
if (
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return "items";
|
|
101
|
-
for (const [key, value] of Object.entries(rec)) {
|
|
102
|
-
if (!key.startsWith("_") && Array.isArray(value)) {
|
|
103
|
-
return key;
|
|
104
|
-
}
|
|
132
|
+
function cloneSpine(obj, path, leafValue) {
|
|
133
|
+
if (path.length === 0)
|
|
134
|
+
return obj;
|
|
135
|
+
if (path.length === 1) {
|
|
136
|
+
return { ...obj, [path[0]]: leafValue };
|
|
105
137
|
}
|
|
106
|
-
|
|
138
|
+
const [head, ...rest] = path;
|
|
139
|
+
return {
|
|
140
|
+
...obj,
|
|
141
|
+
[head]: cloneSpine(obj[head], rest, leafValue),
|
|
142
|
+
};
|
|
107
143
|
}
|
|
108
144
|
/**
|
|
109
145
|
* Build a status message for the response.
|
|
110
|
-
*
|
|
111
|
-
*
|
|
146
|
+
* When maxTokens is provided, uses truncateDeepArray for deep truncation.
|
|
147
|
+
* When maxTokens is omitted, returns COMPLETE with no truncation.
|
|
112
148
|
*/
|
|
113
|
-
export function buildStatusMessage(queryResult,
|
|
149
|
+
export function buildStatusMessage(queryResult, maxTokens) {
|
|
114
150
|
if (typeof queryResult !== "object" || queryResult === null || Array.isArray(queryResult)) {
|
|
115
151
|
return { status: "COMPLETE", result: queryResult };
|
|
116
152
|
}
|
|
117
153
|
const qr = queryResult;
|
|
154
|
+
// No budget specified — return complete, no truncation
|
|
155
|
+
if (maxTokens === undefined) {
|
|
156
|
+
const arrayLen = findPrimaryArrayLength(qr);
|
|
157
|
+
const status = arrayLen !== null ? `COMPLETE (${arrayLen} items)` : "COMPLETE";
|
|
158
|
+
return { status, result: qr };
|
|
159
|
+
}
|
|
160
|
+
// Budget specified — check if truncation needed
|
|
118
161
|
const totalTokens = estimateTokens(qr);
|
|
119
|
-
if (totalTokens <=
|
|
162
|
+
if (totalTokens <= maxTokens) {
|
|
120
163
|
const arrayLen = findPrimaryArrayLength(qr);
|
|
121
164
|
const status = arrayLen !== null ? `COMPLETE (${arrayLen} items)` : "COMPLETE";
|
|
122
165
|
return { status, result: qr };
|
|
123
166
|
}
|
|
124
|
-
// Need truncation
|
|
125
|
-
const { result, originalCount, keptCount } =
|
|
167
|
+
// Need truncation — use deep array truncation
|
|
168
|
+
const { result, originalCount, keptCount } = truncateDeepArray(qr, maxTokens);
|
|
126
169
|
if (originalCount === 0) {
|
|
127
170
|
// No array found but response is over budget
|
|
128
171
|
return {
|
|
129
|
-
status: `COMPLETE (response exceeds token budget ${
|
|
172
|
+
status: `COMPLETE (response exceeds token budget ${maxTokens} — select fewer fields)`,
|
|
130
173
|
result: qr,
|
|
131
174
|
};
|
|
132
175
|
}
|
|
133
|
-
const status = `TRUNCATED — ${keptCount} of ${originalCount} items (token budget ${
|
|
176
|
+
const status = `TRUNCATED — ${keptCount} of ${originalCount} items (token budget ${maxTokens}). Select fewer fields to fit more items. Other array fields may also be truncated by default limits — check *_count fields for actual totals.`;
|
|
134
177
|
return { status, result };
|
|
135
178
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { startAuth, exchangeCode, awaitCallback, storeTokens, getTokens, isTokenExpired, } from "../oauth.js";
|
|
3
|
+
export function registerAuth({ server, config }) {
|
|
4
|
+
if (!config.oauth)
|
|
5
|
+
return;
|
|
6
|
+
server.tool("auth", `Manage OAuth 2.0 authentication for ${config.name}. ` +
|
|
7
|
+
"Use action 'start' to begin the OAuth flow (returns an authorization URL for " +
|
|
8
|
+
"authorization_code flow, or completes token exchange for client_credentials). " +
|
|
9
|
+
"Use action 'exchange' to complete the flow — the callback is captured automatically " +
|
|
10
|
+
"via a localhost server, or you can provide a 'code' manually. " +
|
|
11
|
+
"Use action 'status' to check the current token status.", {
|
|
12
|
+
action: z
|
|
13
|
+
.enum(["start", "exchange", "status"])
|
|
14
|
+
.describe("'start' begins auth flow, 'exchange' completes code exchange, 'status' shows token info"),
|
|
15
|
+
code: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Authorization code from the OAuth provider (optional for 'exchange' — " +
|
|
19
|
+
"if omitted, waits for the localhost callback automatically)"),
|
|
20
|
+
}, async ({ action, code }) => {
|
|
21
|
+
try {
|
|
22
|
+
if (action === "start") {
|
|
23
|
+
const result = await startAuth(config.oauth);
|
|
24
|
+
if ("url" in result) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.stringify({
|
|
30
|
+
message: "Open this URL to authorize. A local callback server is listening. " +
|
|
31
|
+
"After you approve, call auth with action 'exchange' to complete authentication.",
|
|
32
|
+
authorizationUrl: result.url,
|
|
33
|
+
flow: config.oauth.flow,
|
|
34
|
+
}, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// client_credentials: tokens obtained directly
|
|
40
|
+
storeTokens(result.tokens);
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.stringify({
|
|
46
|
+
message: "Authentication successful (client_credentials flow).",
|
|
47
|
+
tokenType: result.tokens.tokenType,
|
|
48
|
+
expiresIn: Math.round((result.tokens.expiresAt - Date.now()) / 1000),
|
|
49
|
+
scope: result.tokens.scope ?? null,
|
|
50
|
+
}, null, 2),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (action === "exchange") {
|
|
56
|
+
const tokens = code
|
|
57
|
+
? await exchangeCode(config.oauth, code)
|
|
58
|
+
: await awaitCallback(config.oauth);
|
|
59
|
+
storeTokens(tokens);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: JSON.stringify({
|
|
65
|
+
message: "Authentication successful.",
|
|
66
|
+
tokenType: tokens.tokenType,
|
|
67
|
+
expiresIn: Math.round((tokens.expiresAt - Date.now()) / 1000),
|
|
68
|
+
hasRefreshToken: !!tokens.refreshToken,
|
|
69
|
+
scope: tokens.scope ?? null,
|
|
70
|
+
}, null, 2),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// action === "status"
|
|
76
|
+
const tokens = getTokens();
|
|
77
|
+
if (!tokens) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: JSON.stringify({
|
|
83
|
+
authenticated: false,
|
|
84
|
+
message: "No tokens stored. Use auth with action 'start' to authenticate.",
|
|
85
|
+
}, null, 2),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const expired = isTokenExpired();
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify({
|
|
96
|
+
authenticated: true,
|
|
97
|
+
tokenType: tokens.tokenType,
|
|
98
|
+
expired,
|
|
99
|
+
expiresIn: Math.round((tokens.expiresAt - Date.now()) / 1000),
|
|
100
|
+
hasRefreshToken: !!tokens.refreshToken,
|
|
101
|
+
scope: tokens.scope ?? null,
|
|
102
|
+
}, null, 2),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{ type: "text", text: JSON.stringify({ error: message }) },
|
|
112
|
+
],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|