@softeria/ms-365-mcp-server 0.152.0 → 0.153.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.
|
@@ -1068,6 +1068,187 @@ describe("graph-tools", () => {
|
|
|
1068
1068
|
expect(payload).not.toHaveProperty("target_resource");
|
|
1069
1069
|
});
|
|
1070
1070
|
});
|
|
1071
|
+
describe("skiptoken cursor", () => {
|
|
1072
|
+
const prevAllowPagination = process.env.MS365_MCP_ALLOW_PAGINATION;
|
|
1073
|
+
afterEach(() => {
|
|
1074
|
+
if (prevAllowPagination === void 0) delete process.env.MS365_MCP_ALLOW_PAGINATION;
|
|
1075
|
+
else process.env.MS365_MCP_ALLOW_PAGINATION = prevAllowPagination;
|
|
1076
|
+
});
|
|
1077
|
+
const callForResult = async (args) => {
|
|
1078
|
+
mockEndpoints.push(makeEndpoint());
|
|
1079
|
+
mockEndpointsJson = [makeConfig()];
|
|
1080
|
+
const graphClient = createMockGraphClient();
|
|
1081
|
+
const server = createMockServer();
|
|
1082
|
+
const { registerGraphTools } = await loadModule();
|
|
1083
|
+
registerGraphTools(server, graphClient);
|
|
1084
|
+
const result = await server.tools.get("test-tool").handler(args);
|
|
1085
|
+
return { result, graphClient };
|
|
1086
|
+
};
|
|
1087
|
+
const callWith = async (args) => {
|
|
1088
|
+
const { graphClient } = await callForResult(args);
|
|
1089
|
+
return graphClient.graphRequest.mock.calls[0][0];
|
|
1090
|
+
};
|
|
1091
|
+
const singleObjectEndpoint = () => makeEndpoint({
|
|
1092
|
+
alias: "get-thing",
|
|
1093
|
+
path: "/me/thing",
|
|
1094
|
+
parameters: [
|
|
1095
|
+
{ name: "select", type: "Query", schema: z.string().optional() },
|
|
1096
|
+
{ name: "expand", type: "Query", schema: z.string().optional() }
|
|
1097
|
+
]
|
|
1098
|
+
});
|
|
1099
|
+
const registerAnd = async (endpoint, config) => {
|
|
1100
|
+
mockEndpoints.push(endpoint);
|
|
1101
|
+
mockEndpointsJson = [config];
|
|
1102
|
+
const server = createMockServer();
|
|
1103
|
+
const { registerGraphTools } = await loadModule();
|
|
1104
|
+
registerGraphTools(server, createMockGraphClient());
|
|
1105
|
+
return server;
|
|
1106
|
+
};
|
|
1107
|
+
it("omits skiptoken on single-object GETs", async () => {
|
|
1108
|
+
const server = await registerAnd(
|
|
1109
|
+
singleObjectEndpoint(),
|
|
1110
|
+
makeConfig({ toolName: "get-thing", pathPattern: "/me/thing" })
|
|
1111
|
+
);
|
|
1112
|
+
expect(server.tools.get("get-thing").schema.skiptoken).toBeUndefined();
|
|
1113
|
+
});
|
|
1114
|
+
it("keeps skiptoken on delta tools that have $top stripped", async () => {
|
|
1115
|
+
const endpoint = makeEndpoint({
|
|
1116
|
+
alias: "list-calendar-events-delta",
|
|
1117
|
+
path: "/me/calendarView/delta"
|
|
1118
|
+
});
|
|
1119
|
+
const server = await registerAnd(
|
|
1120
|
+
endpoint,
|
|
1121
|
+
makeConfig({
|
|
1122
|
+
toolName: "list-calendar-events-delta",
|
|
1123
|
+
pathPattern: "/me/calendarView/delta"
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
const schema = server.tools.get("list-calendar-events-delta").schema;
|
|
1127
|
+
expect(schema.top).toBeUndefined();
|
|
1128
|
+
expect(schema.skiptoken).toBeDefined();
|
|
1129
|
+
});
|
|
1130
|
+
it("advertises skiptoken on GET list tools", async () => {
|
|
1131
|
+
mockEndpoints.push(makeEndpoint());
|
|
1132
|
+
mockEndpointsJson = [makeConfig()];
|
|
1133
|
+
const server = createMockServer();
|
|
1134
|
+
const { registerGraphTools } = await loadModule();
|
|
1135
|
+
registerGraphTools(server, createMockGraphClient());
|
|
1136
|
+
expect(server.tools.get("test-tool").schema.skiptoken).toBeDefined();
|
|
1137
|
+
});
|
|
1138
|
+
it("still advertises skiptoken when MS365_MCP_ALLOW_PAGINATION is disabled", async () => {
|
|
1139
|
+
process.env.MS365_MCP_ALLOW_PAGINATION = "0";
|
|
1140
|
+
mockEndpoints.push(makeEndpoint());
|
|
1141
|
+
mockEndpointsJson = [makeConfig()];
|
|
1142
|
+
const server = createMockServer();
|
|
1143
|
+
const { registerGraphTools } = await loadModule();
|
|
1144
|
+
registerGraphTools(server, createMockGraphClient());
|
|
1145
|
+
expect(server.tools.get("test-tool").schema.fetchAllPages).toBeUndefined();
|
|
1146
|
+
expect(server.tools.get("test-tool").schema.skiptoken).toBeDefined();
|
|
1147
|
+
});
|
|
1148
|
+
it("forwards a bare token as $skiptoken", async () => {
|
|
1149
|
+
const path = await callWith({ skiptoken: "abc123" });
|
|
1150
|
+
expect(path).toContain("$skiptoken=abc123");
|
|
1151
|
+
});
|
|
1152
|
+
it("does not double-encode a token copied from @odata.nextLink", async () => {
|
|
1153
|
+
const path = await callWith({ skiptoken: "eyJhIjoxfQ%3d%3d" });
|
|
1154
|
+
expect(path).toContain("$skiptoken=eyJhIjoxfQ%3D%3D");
|
|
1155
|
+
expect(path).not.toContain("%253");
|
|
1156
|
+
});
|
|
1157
|
+
it("extracts the token when handed a whole nextLink URL", async () => {
|
|
1158
|
+
const path = await callWith({
|
|
1159
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/chats?$top=5&$filter=x&$skiptoken=tok123"
|
|
1160
|
+
});
|
|
1161
|
+
expect(path).toContain("$skiptoken=tok123");
|
|
1162
|
+
expect(path).not.toContain("graph.microsoft.com");
|
|
1163
|
+
});
|
|
1164
|
+
it("keeps only the cursor when the nextLink has params after it", async () => {
|
|
1165
|
+
const path = await callWith({ skiptoken: "$skiptoken=tok123&$top=5" });
|
|
1166
|
+
expect(path).toContain("$skiptoken=tok123");
|
|
1167
|
+
expect(path).not.toContain("tok123&");
|
|
1168
|
+
});
|
|
1169
|
+
it("accepts the $-prefixed param name", async () => {
|
|
1170
|
+
const path = await callWith({ $skiptoken: "abc123" });
|
|
1171
|
+
expect(path).toContain("$skiptoken=abc123");
|
|
1172
|
+
});
|
|
1173
|
+
it.each(["", " "])("omits $skiptoken entirely when blank (%j)", async (blank) => {
|
|
1174
|
+
const path = await callWith({ skiptoken: blank });
|
|
1175
|
+
expect(path).not.toContain("skiptoken");
|
|
1176
|
+
});
|
|
1177
|
+
it("sends $skip when the nextLink pages with $skip", async () => {
|
|
1178
|
+
const path = await callWith({
|
|
1179
|
+
top: 10,
|
|
1180
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/messages?$top=10&$skip=10"
|
|
1181
|
+
});
|
|
1182
|
+
expect(path).toContain("$skip=10");
|
|
1183
|
+
expect(path).not.toContain("skiptoken");
|
|
1184
|
+
});
|
|
1185
|
+
it("ignores a cursor-looking value inside another query param", async () => {
|
|
1186
|
+
const path = await callWith({
|
|
1187
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/messages?$filter=subject eq '%24skip=100'&$skip=10"
|
|
1188
|
+
});
|
|
1189
|
+
expect(path).toContain("$skip=10");
|
|
1190
|
+
expect(path).not.toContain("100");
|
|
1191
|
+
});
|
|
1192
|
+
it("refuses a link with no cursor to page with", async () => {
|
|
1193
|
+
const { result, graphClient } = await callForResult({
|
|
1194
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/delta()?$deltatoken=abc"
|
|
1195
|
+
});
|
|
1196
|
+
expect(result.isError).toBe(true);
|
|
1197
|
+
expect(JSON.parse(result.content[0].text).error).toBe("invalid_skiptoken");
|
|
1198
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1199
|
+
});
|
|
1200
|
+
it("refuses a delta token= link without calling it a deltaLink", async () => {
|
|
1201
|
+
const { result, graphClient } = await callForResult({
|
|
1202
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/drive/delta(token=1230919asd190410jlka)"
|
|
1203
|
+
});
|
|
1204
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1205
|
+
expect(payload.error).toBe("invalid_skiptoken");
|
|
1206
|
+
expect(payload.message).not.toContain("deltaLink");
|
|
1207
|
+
expect(payload.message).toContain("fetchAllPages");
|
|
1208
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1209
|
+
});
|
|
1210
|
+
it("refuses a link whose $skiptoken carries no value", async () => {
|
|
1211
|
+
const { result } = await callForResult({
|
|
1212
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/chats?$skiptoken=&$top=5"
|
|
1213
|
+
});
|
|
1214
|
+
expect(result.isError).toBe(true);
|
|
1215
|
+
expect(JSON.parse(result.content[0].text).error).toBe("invalid_skiptoken");
|
|
1216
|
+
});
|
|
1217
|
+
it("accepts an encoded %24skiptoken marker", async () => {
|
|
1218
|
+
const path = await callWith({
|
|
1219
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/chats?%24top=5&%24skiptoken=tok123"
|
|
1220
|
+
});
|
|
1221
|
+
expect(path).toContain("$skiptoken=tok123");
|
|
1222
|
+
});
|
|
1223
|
+
it.each([
|
|
1224
|
+
["https://graph.microsoft.com/v1.0/me/messages?$top=10&$skip=0", "$skip=0"],
|
|
1225
|
+
["https://graph.microsoft.com/v1.0/me/messages?%24top=10&%24skip=10", "$skip=10"]
|
|
1226
|
+
])("reads the $skip cursor out of %s", async (link, expected) => {
|
|
1227
|
+
const path = await callWith({ skiptoken: link });
|
|
1228
|
+
expect(path).toContain(expected);
|
|
1229
|
+
});
|
|
1230
|
+
it("refuses a $skip cursor that is not a plain number", async () => {
|
|
1231
|
+
const { result } = await callForResult({
|
|
1232
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/messages?$skip=10junk"
|
|
1233
|
+
});
|
|
1234
|
+
expect(JSON.parse(result.content[0].text).error).toBe("invalid_skiptoken");
|
|
1235
|
+
});
|
|
1236
|
+
it("offers no fetchAllPages in the refusal when pagination is disabled", async () => {
|
|
1237
|
+
process.env.MS365_MCP_ALLOW_PAGINATION = "0";
|
|
1238
|
+
const { result } = await callForResult({
|
|
1239
|
+
skiptoken: "https://graph.microsoft.com/v1.0/me/drive/delta(token=1230919asd190410jlka)"
|
|
1240
|
+
});
|
|
1241
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1242
|
+
expect(payload.error).toBe("invalid_skiptoken");
|
|
1243
|
+
expect(payload.message).not.toContain("fetchAllPages");
|
|
1244
|
+
});
|
|
1245
|
+
it("sends the cursor alongside the original query options", async () => {
|
|
1246
|
+
const path = await callWith({ filter: "chatType eq 'oneOnOne'", top: 5, skiptoken: "tok" });
|
|
1247
|
+
expect(path).toContain("$filter=");
|
|
1248
|
+
expect(path).toContain("$top=5");
|
|
1249
|
+
expect(path).toContain("$skiptoken=tok");
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1071
1252
|
describe("fetchAllPages pagination", () => {
|
|
1072
1253
|
it("should follow @odata.nextLink and combine results", async () => {
|
|
1073
1254
|
const endpoint = makeEndpoint();
|
package/dist/graph-tools.js
CHANGED
|
@@ -56,7 +56,9 @@ import {
|
|
|
56
56
|
EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
|
|
57
57
|
getAcceptParamDescription,
|
|
58
58
|
getAccountParamDescription,
|
|
59
|
-
getFetchAllPagesParamDescription
|
|
59
|
+
getFetchAllPagesParamDescription,
|
|
60
|
+
SKIPTOKEN_PARAM_DESCRIPTION,
|
|
61
|
+
isSkiptokenApplicable
|
|
60
62
|
} from "./lib/param-descriptions.js";
|
|
61
63
|
const __filename = fileURLToPath(import.meta.url);
|
|
62
64
|
const __dirname = path.dirname(__filename);
|
|
@@ -222,6 +224,50 @@ function normalizeSearchQueryParam(queryParams, path2, toolAlias) {
|
|
|
222
224
|
queryParams["$search"] = normalized;
|
|
223
225
|
}
|
|
224
226
|
}
|
|
227
|
+
function normalizeSkiptokenQueryParam(queryParams, toolAlias) {
|
|
228
|
+
const raw = queryParams["$skiptoken"];
|
|
229
|
+
if (raw === void 0) return;
|
|
230
|
+
delete queryParams["$skiptoken"];
|
|
231
|
+
let token = raw.trim();
|
|
232
|
+
if (token === "") return;
|
|
233
|
+
const cursorlessLink = () => {
|
|
234
|
+
logger.warn(`Refusing ${toolAlias}: 'skiptoken' has no $skiptoken or $skip to page with`);
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: JSON.stringify({
|
|
240
|
+
error: "invalid_skiptoken",
|
|
241
|
+
tool: toolAlias,
|
|
242
|
+
message: "The value passed as skiptoken has no $skiptoken or $skip to page with. The drive and sites delta tools page with a token= link, which skiptoken cannot resend." + (paginationAllowed() ? " Remove skiptoken and retry with fetchAllPages set to true to follow the link, or remove it for the first page." : " Remove skiptoken and retry for the first page.")
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
],
|
|
246
|
+
isError: true
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const marker = token.match(/(?:^|[?&])(?:\$|%24)skiptoken=/i);
|
|
250
|
+
if (marker?.index !== void 0) {
|
|
251
|
+
token = token.slice(marker.index + marker[0].length).split("&")[0];
|
|
252
|
+
if (token === "") return cursorlessLink();
|
|
253
|
+
} else if (/:\/\/|\?|^(?:\$|%24)\w+=/.test(token)) {
|
|
254
|
+
const skip = token.match(/(?:^|[?&])(?:\$|%24)skip=(\d+)(?=&|$)/i)?.[1];
|
|
255
|
+
if (skip === void 0) return cursorlessLink();
|
|
256
|
+
logger.info(
|
|
257
|
+
`Auto-corrected parameter 'skiptoken': link pages with $skip, sending $skip=${skip}`
|
|
258
|
+
);
|
|
259
|
+
queryParams["$skip"] = skip;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (token.includes("%")) {
|
|
263
|
+
try {
|
|
264
|
+
token = decodeURIComponent(token);
|
|
265
|
+
} catch {
|
|
266
|
+
logger.warn("skiptoken looks percent-encoded but could not be decoded; sending as-is");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
queryParams["$skiptoken"] = token;
|
|
270
|
+
}
|
|
225
271
|
const DEFAULT_MAX_ITEMS = 1e4;
|
|
226
272
|
function isConfirmGateEnabled() {
|
|
227
273
|
return process.env.MS365_MCP_REQUIRE_CONFIRM === "true";
|
|
@@ -1143,6 +1189,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1143
1189
|
"expand",
|
|
1144
1190
|
"orderby",
|
|
1145
1191
|
"skip",
|
|
1192
|
+
"skiptoken",
|
|
1146
1193
|
"top",
|
|
1147
1194
|
"count",
|
|
1148
1195
|
"search",
|
|
@@ -1261,6 +1308,8 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1261
1308
|
if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
|
|
1262
1309
|
delete queryParams["$top"];
|
|
1263
1310
|
}
|
|
1311
|
+
const skiptokenError = normalizeSkiptokenQueryParam(queryParams, tool.alias);
|
|
1312
|
+
if (skiptokenError) return skiptokenError;
|
|
1264
1313
|
clampTopQueryParam(queryParams);
|
|
1265
1314
|
const searchError = normalizeSearchQueryParam(queryParams, tool.path, tool.alias);
|
|
1266
1315
|
if (searchError) return searchError;
|
|
@@ -1600,6 +1649,9 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
1600
1649
|
const maxPages = getMaxPages();
|
|
1601
1650
|
paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages)).optional();
|
|
1602
1651
|
}
|
|
1652
|
+
if (isSkiptokenApplicable(tool, Object.keys(paramSchema))) {
|
|
1653
|
+
paramSchema["skiptoken"] = z.string().describe(SKIPTOKEN_PARAM_DESCRIPTION).optional();
|
|
1654
|
+
}
|
|
1603
1655
|
if (paramSchema["filter"] !== void 0 || paramSchema["$filter"] !== void 0) {
|
|
1604
1656
|
const key = paramSchema["$filter"] !== void 0 ? "$filter" : "filter";
|
|
1605
1657
|
paramSchema[key] = z.string().describe(FILTER_PARAM_DESCRIPTION).optional();
|
|
@@ -35,6 +35,15 @@ const EXPAND_PARAM_DESCRIPTION = 'Navigation properties to inline, e.g. attachme
|
|
|
35
35
|
const ORDERBY_PARAM_DESCRIPTION = "Sort expression, e.g. receivedDateTime desc";
|
|
36
36
|
const TOP_PARAM_DESCRIPTION = "Page size (Graph $top). Start small (e.g. 5\u201315) so responses fit the model context; raise only if needed. Use $select to return fewer fields per item. For more rows, use @odata.nextLink from the response instead of a very large $top.";
|
|
37
37
|
const SKIP_PARAM_DESCRIPTION = "Items to skip for pagination. Not supported with $search.";
|
|
38
|
+
const SKIPTOKEN_PARAM_DESCRIPTION = "Next page: the @odata.nextLink from the previous response. Keep the other arguments the same.";
|
|
39
|
+
const COLLECTION_QUERY_PARAMS = /* @__PURE__ */ new Set(["top", "filter", "orderby", "count"]);
|
|
40
|
+
function isSkiptokenApplicable(tool, paramNames) {
|
|
41
|
+
if (tool.method.toUpperCase() !== "GET") return false;
|
|
42
|
+
for (const name of paramNames) {
|
|
43
|
+
if (COLLECTION_QUERY_PARAMS.has(name.replace(/^\$/, "").toLowerCase())) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
38
47
|
const COUNT_PARAM_DESCRIPTION = "Set true to enable advanced query mode (ConsistencyLevel: eventual). Required for complex $filter on flag/flagStatus or contains().";
|
|
39
48
|
const CONFIRM_PARAM_DESCRIPTION = 'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.';
|
|
40
49
|
const TIMEZONE_PARAM_DESCRIPTION = 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.';
|
|
@@ -81,6 +90,7 @@ export {
|
|
|
81
90
|
ORDERBY_PARAM_DESCRIPTION,
|
|
82
91
|
SEARCH_PARAM_DESCRIPTION,
|
|
83
92
|
SELECT_PARAM_DESCRIPTION,
|
|
93
|
+
SKIPTOKEN_PARAM_DESCRIPTION,
|
|
84
94
|
SKIP_PARAM_DESCRIPTION,
|
|
85
95
|
TIMEZONE_PARAM_DESCRIPTION,
|
|
86
96
|
TOP_PARAM_DESCRIPTION,
|
|
@@ -91,6 +101,7 @@ export {
|
|
|
91
101
|
getMaxPages,
|
|
92
102
|
getODataParamDescription,
|
|
93
103
|
isFetchAllPagesApplicable,
|
|
104
|
+
isSkiptokenApplicable,
|
|
94
105
|
paginationAllowed,
|
|
95
106
|
positiveIntFromEnv,
|
|
96
107
|
shouldOmitTopParam
|
package/dist/lib/tool-schema.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
getODataParamDescription,
|
|
5
5
|
shouldOmitTopParam,
|
|
6
6
|
isFetchAllPagesApplicable,
|
|
7
|
+
isSkiptokenApplicable,
|
|
8
|
+
SKIPTOKEN_PARAM_DESCRIPTION,
|
|
7
9
|
getMaxPages,
|
|
8
10
|
getFetchAllPagesParamDescription,
|
|
9
11
|
getAccountParamDescription,
|
|
@@ -57,6 +59,18 @@ function describeToolSchema(tool, config, ctx = {}) {
|
|
|
57
59
|
schema: { type: "boolean" }
|
|
58
60
|
});
|
|
59
61
|
}
|
|
62
|
+
if (isSkiptokenApplicable(
|
|
63
|
+
{ method: tool.method },
|
|
64
|
+
params.map((p) => p.name)
|
|
65
|
+
)) {
|
|
66
|
+
params.push({
|
|
67
|
+
name: "skiptoken",
|
|
68
|
+
in: "Query",
|
|
69
|
+
required: false,
|
|
70
|
+
description: SKIPTOKEN_PARAM_DESCRIPTION,
|
|
71
|
+
schema: { type: "string" }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
60
74
|
if (ctx.multiAccount) {
|
|
61
75
|
params.push({
|
|
62
76
|
name: "account",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.153.1",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|