@authn8/mcp-server 1.0.5 → 1.1.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.
- package/README.md +41 -16
- package/dist/index.js +132 -143
- package/package.json +11 -7
- package/Dockerfile +0 -6
- package/src/client.ts +0 -156
- package/src/index.ts +0 -125
- package/src/tools.ts +0 -168
- package/tsconfig.json +0 -16
- package/tsup.config.ts +0 -12
package/README.md
CHANGED
|
@@ -92,19 +92,24 @@ Add to your Cursor MCP settings:
|
|
|
92
92
|
|
|
93
93
|
## Available Tools
|
|
94
94
|
|
|
95
|
+
All tools declare an output schema and return `structuredContent` alongside the
|
|
96
|
+
text block, so clients get typed results without re-parsing JSON out of text.
|
|
97
|
+
|
|
95
98
|
### list_accounts
|
|
96
99
|
|
|
97
100
|
Returns all 2FA accounts accessible to this token.
|
|
98
101
|
|
|
99
102
|
**Example response:**
|
|
100
103
|
```json
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
{
|
|
105
|
+
"accounts": [
|
|
106
|
+
{
|
|
107
|
+
"id": "924c52a6-4457-4970-a39f-4dc620217683",
|
|
108
|
+
"name": "AWS Production",
|
|
109
|
+
"issuerDomain": "amazon.com"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
108
113
|
```
|
|
109
114
|
|
|
110
115
|
### get_otp
|
|
@@ -113,15 +118,33 @@ Generates a TOTP code for a specific account.
|
|
|
113
118
|
|
|
114
119
|
**Parameters:**
|
|
115
120
|
- `account_id` (string, optional) - UUID of the account
|
|
116
|
-
- `account_name` (string, optional) - Name to search for (partial match)
|
|
121
|
+
- `account_name` (string, optional) - Name or issuer domain to search for (case-insensitive partial match)
|
|
117
122
|
|
|
118
|
-
Provide either `account_id` or `account_name`. If multiple accounts match the
|
|
123
|
+
Provide either `account_id` or `account_name`. If multiple accounts match the
|
|
124
|
+
name, the tool returns them in `multipleMatches` so you can retry with an
|
|
125
|
+
`account_id`.
|
|
119
126
|
|
|
120
127
|
**Example response:**
|
|
121
128
|
```json
|
|
122
129
|
{
|
|
123
|
-
"
|
|
124
|
-
"code": "483920"
|
|
130
|
+
"name": "AWS Production",
|
|
131
|
+
"code": "483920",
|
|
132
|
+
"length": 6
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Ambiguous match:**
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"error": "Multiple accounts match 'aws'. Please be more specific or use account_id.",
|
|
140
|
+
"length": 0,
|
|
141
|
+
"multipleMatches": [
|
|
142
|
+
{
|
|
143
|
+
"id": "924c52a6-4457-4970-a39f-4dc620217683",
|
|
144
|
+
"name": "AWS Production",
|
|
145
|
+
"issuerDomain": "amazon.com"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
125
148
|
}
|
|
126
149
|
```
|
|
127
150
|
|
|
@@ -132,11 +155,13 @@ Returns information about the current token.
|
|
|
132
155
|
**Example response:**
|
|
133
156
|
```json
|
|
134
157
|
{
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
"tokenName": "MCP Server 2",
|
|
159
|
+
"businessName": "Bytecode Solutions",
|
|
160
|
+
"scopedGroups": [
|
|
161
|
+
{ "id": "cfeba14a-e990-49e8-b6e5-61d63f7d68c8", "name": "HR" }
|
|
162
|
+
],
|
|
163
|
+
"expiresAt": "2025-12-25T23:59:59Z",
|
|
164
|
+
"accountCount": 1
|
|
140
165
|
}
|
|
141
166
|
```
|
|
142
167
|
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
var require2 = createRequire(import.meta.url);
|
|
10
|
+
var { version } = require2("../package.json");
|
|
8
11
|
var API_URL = process.env.AUTHN8_API_URL || "https://api.authn8.com";
|
|
9
12
|
var API_KEY = process.env.AUTHN8_API_KEY;
|
|
10
|
-
var USER_AGENT =
|
|
13
|
+
var USER_AGENT = `authn8-mcp/${version}`;
|
|
14
|
+
var PACKAGE_VERSION = version;
|
|
11
15
|
var Authn8Error = class extends Error {
|
|
12
16
|
constructor(message, statusCode) {
|
|
13
17
|
super(message);
|
|
@@ -65,8 +69,8 @@ async function apiRequest(endpoint, method = "GET") {
|
|
|
65
69
|
async function getTokenInfo() {
|
|
66
70
|
return apiRequest("/api/pat/me");
|
|
67
71
|
}
|
|
68
|
-
async function listAccounts() {
|
|
69
|
-
if (accountsCache && Date.now() - accountsCache.timestamp < CACHE_TTL_MS) {
|
|
72
|
+
async function listAccounts(forceRefresh = false) {
|
|
73
|
+
if (!forceRefresh && accountsCache && Date.now() - accountsCache.timestamp < CACHE_TTL_MS) {
|
|
70
74
|
return accountsCache.accounts;
|
|
71
75
|
}
|
|
72
76
|
const response = await apiRequest("/api/pat/accounts");
|
|
@@ -76,25 +80,25 @@ async function listAccounts() {
|
|
|
76
80
|
};
|
|
77
81
|
return response.accounts;
|
|
78
82
|
}
|
|
83
|
+
function servedFromCache() {
|
|
84
|
+
return accountsCache !== null && Date.now() - accountsCache.timestamp < CACHE_TTL_MS;
|
|
85
|
+
}
|
|
79
86
|
async function getOtp(accountId) {
|
|
80
87
|
return apiRequest(`/api/pat/otp/${accountId}`);
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
const accounts = await listAccounts();
|
|
89
|
+
function matchAccounts(accounts, name) {
|
|
84
90
|
const lowerName = name.toLowerCase();
|
|
85
|
-
|
|
91
|
+
return accounts.filter(
|
|
86
92
|
(acc) => acc.name?.toLowerCase().includes(lowerName) || acc.issuerDomain?.toLowerCase().includes(lowerName)
|
|
87
93
|
);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (matches.length === 1) {
|
|
95
|
-
return { account: matches[0] };
|
|
94
|
+
}
|
|
95
|
+
async function findAccountByName(name) {
|
|
96
|
+
const accounts = await listAccounts();
|
|
97
|
+
const matches = matchAccounts(accounts, name);
|
|
98
|
+
if (matches.length > 0 || !servedFromCache()) {
|
|
99
|
+
return matches;
|
|
96
100
|
}
|
|
97
|
-
return
|
|
101
|
+
return matchAccounts(await listAccounts(true), name);
|
|
98
102
|
}
|
|
99
103
|
async function validateToken() {
|
|
100
104
|
try {
|
|
@@ -111,131 +115,121 @@ async function validateToken() {
|
|
|
111
115
|
|
|
112
116
|
// src/tools.ts
|
|
113
117
|
import { z } from "zod";
|
|
118
|
+
var accountShape = z.object({
|
|
119
|
+
id: z.uuid(),
|
|
120
|
+
name: z.string(),
|
|
121
|
+
issuerDomain: z.string().nullable()
|
|
122
|
+
});
|
|
123
|
+
var listAccountsOutput = z.object({
|
|
124
|
+
error: z.string().optional(),
|
|
125
|
+
accounts: z.array(accountShape).optional()
|
|
126
|
+
});
|
|
127
|
+
var getOtpOutput = z.object({
|
|
128
|
+
error: z.string().optional(),
|
|
129
|
+
name: z.string().optional(),
|
|
130
|
+
code: z.string().optional(),
|
|
131
|
+
length: z.number().int(),
|
|
132
|
+
multipleMatches: z.array(accountShape).optional()
|
|
133
|
+
});
|
|
134
|
+
var whoamiOutput = z.object({
|
|
135
|
+
error: z.string().optional(),
|
|
136
|
+
tokenName: z.string().optional(),
|
|
137
|
+
businessName: z.string().optional(),
|
|
138
|
+
scopedGroups: z.array(z.object({ id: z.uuid(), name: z.string() })).optional(),
|
|
139
|
+
expiresAt: z.string().optional(),
|
|
140
|
+
accountCount: z.number().int()
|
|
141
|
+
});
|
|
114
142
|
var toolDefinitions = {
|
|
115
143
|
list_accounts: {
|
|
116
|
-
description: "Returns all 2FA accounts accessible to this token. Use this to see what accounts are available before requesting an OTP code.",
|
|
117
|
-
inputSchema: z.object({})
|
|
144
|
+
description: "Returns all 2FA accounts accessible to this token. Use this to see what accounts are available before requesting an OTP code. Each account includes: id (UUID for get_otp), name, and issuerDomain. Returns an empty list if no accounts are accessible.",
|
|
145
|
+
inputSchema: z.object({}),
|
|
146
|
+
outputSchema: listAccountsOutput
|
|
118
147
|
},
|
|
119
148
|
get_otp: {
|
|
120
|
-
description: "Generates a TOTP code for a specific account.
|
|
149
|
+
description: "Generates a TOTP code for a specific account. REQUIRED: Provide either account_id OR account_name (at least one must be specified). Errors: Returns 'No account found' if the name doesn't match any account. Returns 'Multiple accounts match' with a list of matches in the 'multipleMatches' field if the name is ambiguous - use the returned account_id to retry.",
|
|
121
150
|
inputSchema: z.object({
|
|
122
|
-
account_id: z.uuid().optional().describe(
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
account_id: z.uuid().optional().describe(
|
|
152
|
+
"UUID of the account. Use this for exact matching, especially after resolving ambiguous name matches."
|
|
153
|
+
),
|
|
154
|
+
account_name: z.string().optional().describe(
|
|
155
|
+
"Name or issuer domain to search for (case-insensitive partial match). If multiple accounts match, returns a list to choose from."
|
|
156
|
+
)
|
|
157
|
+
}),
|
|
158
|
+
outputSchema: getOtpOutput
|
|
125
159
|
},
|
|
126
160
|
whoami: {
|
|
127
161
|
description: "Returns information about the current token and what it has access to, including the business name, token name, scoped groups, account count, and expiration date.",
|
|
128
|
-
inputSchema: z.object({})
|
|
162
|
+
inputSchema: z.object({}),
|
|
163
|
+
outputSchema: whoamiOutput
|
|
129
164
|
}
|
|
130
165
|
};
|
|
131
|
-
|
|
132
|
-
const accounts = await listAccounts();
|
|
133
|
-
if (accounts.length === 0) {
|
|
134
|
-
return {
|
|
135
|
-
content: [
|
|
136
|
-
{
|
|
137
|
-
type: "text",
|
|
138
|
-
text: "No accounts are accessible with this token."
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const formatted = accounts.map((acc) => ({
|
|
144
|
-
id: acc.id,
|
|
145
|
-
name: acc.name,
|
|
146
|
-
issuer: acc.issuerDomain || null
|
|
147
|
-
}));
|
|
166
|
+
function result(payload) {
|
|
148
167
|
return {
|
|
149
|
-
content: [
|
|
150
|
-
|
|
151
|
-
type: "text",
|
|
152
|
-
text: JSON.stringify(formatted, null, 2)
|
|
153
|
-
}
|
|
154
|
-
]
|
|
168
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
169
|
+
structuredContent: payload
|
|
155
170
|
};
|
|
156
171
|
}
|
|
172
|
+
async function handleListAccounts() {
|
|
173
|
+
const accounts = await listAccounts();
|
|
174
|
+
return result({
|
|
175
|
+
accounts: accounts.map((acc) => ({
|
|
176
|
+
id: acc.id,
|
|
177
|
+
name: acc.name,
|
|
178
|
+
issuerDomain: acc.issuerDomain ?? null
|
|
179
|
+
}))
|
|
180
|
+
});
|
|
181
|
+
}
|
|
157
182
|
async function handleGetOtp(args) {
|
|
158
183
|
const { account_id, account_name } = args;
|
|
159
184
|
if (!account_id && !account_name) {
|
|
160
|
-
return {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
text: "Error: Either account_id or account_name must be provided."
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
};
|
|
185
|
+
return result({
|
|
186
|
+
error: "Either account_id or account_name must be provided",
|
|
187
|
+
length: 0
|
|
188
|
+
});
|
|
168
189
|
}
|
|
169
190
|
let targetAccountId;
|
|
170
|
-
let accountName;
|
|
171
191
|
if (account_id) {
|
|
172
192
|
targetAccountId = account_id;
|
|
173
|
-
const accounts = await listAccounts();
|
|
174
|
-
const account = accounts.find((a) => a.id === account_id);
|
|
175
|
-
if (!account) {
|
|
176
|
-
return {
|
|
177
|
-
content: [
|
|
178
|
-
{
|
|
179
|
-
type: "text",
|
|
180
|
-
text: `Error: Account with ID "${account_id}" not found. Use list_accounts to see available accounts.`
|
|
181
|
-
}
|
|
182
|
-
]
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
accountName = account.name;
|
|
186
193
|
} else {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
return {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
const matches = await findAccountByName(account_name);
|
|
195
|
+
if (matches.length === 0) {
|
|
196
|
+
return result({
|
|
197
|
+
error: `No account found matching '${account_name}'. Use list_accounts to see available accounts.`,
|
|
198
|
+
length: 0
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (matches.length > 1) {
|
|
202
|
+
return result({
|
|
203
|
+
error: `Multiple accounts match '${account_name}'. Please be more specific or use account_id.`,
|
|
204
|
+
length: 0,
|
|
205
|
+
multipleMatches: matches.map((a) => ({
|
|
206
|
+
id: a.id,
|
|
207
|
+
name: a.name,
|
|
208
|
+
issuerDomain: a.issuerDomain ?? null
|
|
209
|
+
}))
|
|
210
|
+
});
|
|
198
211
|
}
|
|
199
|
-
targetAccountId =
|
|
200
|
-
accountName = result.account.name;
|
|
212
|
+
targetAccountId = matches[0].id;
|
|
201
213
|
}
|
|
202
214
|
const otp = await getOtp(targetAccountId);
|
|
203
|
-
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
account: accountName,
|
|
210
|
-
code: otp.code
|
|
211
|
-
},
|
|
212
|
-
null,
|
|
213
|
-
2
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
]
|
|
217
|
-
};
|
|
215
|
+
return result({
|
|
216
|
+
name: otp.name,
|
|
217
|
+
code: otp.code,
|
|
218
|
+
length: otp.length
|
|
219
|
+
});
|
|
218
220
|
}
|
|
219
221
|
async function handleWhoami() {
|
|
220
222
|
const info = await getTokenInfo();
|
|
221
|
-
return {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
expires_at: info.expiresAt
|
|
232
|
-
},
|
|
233
|
-
null,
|
|
234
|
-
2
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
]
|
|
238
|
-
};
|
|
223
|
+
return result({
|
|
224
|
+
tokenName: info.tokenName,
|
|
225
|
+
businessName: info.businessName,
|
|
226
|
+
scopedGroups: (info.scopedGroups ?? []).map((g) => ({
|
|
227
|
+
id: g.id,
|
|
228
|
+
name: g.name
|
|
229
|
+
})),
|
|
230
|
+
expiresAt: info.expiresAt,
|
|
231
|
+
accountCount: info.accountCount
|
|
232
|
+
});
|
|
239
233
|
}
|
|
240
234
|
|
|
241
235
|
// src/index.ts
|
|
@@ -248,13 +242,24 @@ function formatDate(isoDate) {
|
|
|
248
242
|
});
|
|
249
243
|
}
|
|
250
244
|
function displayStartupInfo(info) {
|
|
251
|
-
console.error(
|
|
245
|
+
console.error(`Authn8 MCP Server v${PACKAGE_VERSION}`);
|
|
252
246
|
console.error(`Business: ${info.businessName}`);
|
|
253
247
|
console.error(`Token: ${info.tokenName}`);
|
|
254
248
|
console.error(`Accounts: ${info.accountCount}`);
|
|
255
249
|
console.error(`Expires: ${formatDate(info.expiresAt)}`);
|
|
256
250
|
console.error("");
|
|
257
251
|
}
|
|
252
|
+
function toErrorResult(error) {
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
isError: true
|
|
261
|
+
};
|
|
262
|
+
}
|
|
258
263
|
async function main() {
|
|
259
264
|
let tokenInfo;
|
|
260
265
|
try {
|
|
@@ -269,26 +274,20 @@ async function main() {
|
|
|
269
274
|
displayStartupInfo(tokenInfo);
|
|
270
275
|
const server = new McpServer({
|
|
271
276
|
name: "authn8-mcp",
|
|
272
|
-
version:
|
|
277
|
+
version: PACKAGE_VERSION
|
|
273
278
|
});
|
|
274
279
|
server.registerTool(
|
|
275
280
|
"list_accounts",
|
|
276
281
|
{
|
|
277
282
|
description: toolDefinitions.list_accounts.description,
|
|
278
|
-
inputSchema: toolDefinitions.list_accounts.inputSchema
|
|
283
|
+
inputSchema: toolDefinitions.list_accounts.inputSchema,
|
|
284
|
+
outputSchema: toolDefinitions.list_accounts.outputSchema
|
|
279
285
|
},
|
|
280
286
|
async () => {
|
|
281
287
|
try {
|
|
282
288
|
return await handleListAccounts();
|
|
283
289
|
} catch (error) {
|
|
284
|
-
return
|
|
285
|
-
content: [
|
|
286
|
-
{
|
|
287
|
-
type: "text",
|
|
288
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
289
|
-
}
|
|
290
|
-
]
|
|
291
|
-
};
|
|
290
|
+
return toErrorResult(error);
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
293
|
);
|
|
@@ -296,20 +295,16 @@ async function main() {
|
|
|
296
295
|
"get_otp",
|
|
297
296
|
{
|
|
298
297
|
description: toolDefinitions.get_otp.description,
|
|
299
|
-
inputSchema: toolDefinitions.get_otp.inputSchema
|
|
298
|
+
inputSchema: toolDefinitions.get_otp.inputSchema,
|
|
299
|
+
outputSchema: toolDefinitions.get_otp.outputSchema
|
|
300
300
|
},
|
|
301
301
|
async (args) => {
|
|
302
302
|
try {
|
|
303
|
-
return await handleGetOtp(
|
|
303
|
+
return await handleGetOtp(
|
|
304
|
+
args
|
|
305
|
+
);
|
|
304
306
|
} catch (error) {
|
|
305
|
-
return
|
|
306
|
-
content: [
|
|
307
|
-
{
|
|
308
|
-
type: "text",
|
|
309
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
310
|
-
}
|
|
311
|
-
]
|
|
312
|
-
};
|
|
307
|
+
return toErrorResult(error);
|
|
313
308
|
}
|
|
314
309
|
}
|
|
315
310
|
);
|
|
@@ -317,20 +312,14 @@ async function main() {
|
|
|
317
312
|
"whoami",
|
|
318
313
|
{
|
|
319
314
|
description: toolDefinitions.whoami.description,
|
|
320
|
-
inputSchema: toolDefinitions.whoami.inputSchema
|
|
315
|
+
inputSchema: toolDefinitions.whoami.inputSchema,
|
|
316
|
+
outputSchema: toolDefinitions.whoami.outputSchema
|
|
321
317
|
},
|
|
322
318
|
async () => {
|
|
323
319
|
try {
|
|
324
320
|
return await handleWhoami();
|
|
325
321
|
} catch (error) {
|
|
326
|
-
return
|
|
327
|
-
content: [
|
|
328
|
-
{
|
|
329
|
-
type: "text",
|
|
330
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
331
|
-
}
|
|
332
|
-
]
|
|
333
|
-
};
|
|
322
|
+
return toErrorResult(error);
|
|
334
323
|
}
|
|
335
324
|
}
|
|
336
325
|
);
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authn8/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "MCP server for Authn8 2FA code access",
|
|
5
|
+
"mcpName": "io.github.authn8/mcp-server",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
|
-
"authn8-mcp": "
|
|
8
|
+
"authn8-mcp": "dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
10
14
|
"scripts": {
|
|
11
15
|
"build": "tsup src/index.ts --format esm --dts",
|
|
12
|
-
"dev": "
|
|
16
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
13
17
|
"start": "node dist/index.js"
|
|
14
18
|
},
|
|
15
19
|
"keywords": [
|
|
@@ -21,13 +25,13 @@
|
|
|
21
25
|
],
|
|
22
26
|
"repository": {
|
|
23
27
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/authn8/mcp-server"
|
|
28
|
+
"url": "git+https://github.com/authn8/mcp-server.git"
|
|
25
29
|
},
|
|
26
30
|
"homepage": "https://authn8.com",
|
|
27
|
-
"author": "",
|
|
28
|
-
"license": "
|
|
31
|
+
"author": "Authn8",
|
|
32
|
+
"license": "MIT",
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
31
35
|
"zod": "^4.1.13"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
package/Dockerfile
DELETED
package/src/client.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
const API_URL = process.env.AUTHN8_API_URL || "https://api.authn8.com";
|
|
2
|
-
const API_KEY = process.env.AUTHN8_API_KEY;
|
|
3
|
-
const USER_AGENT = "authn8-mcp/1.0.0";
|
|
4
|
-
|
|
5
|
-
export interface TokenInfo {
|
|
6
|
-
businessName: string;
|
|
7
|
-
tokenName: string;
|
|
8
|
-
scopedGroups: Array<{ id: string; name: string }>;
|
|
9
|
-
accountCount: number;
|
|
10
|
-
expiresAt: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface Account {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
issuerDomain: string | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface AccountsResponse {
|
|
20
|
-
accounts: Account[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface OtpResponse {
|
|
24
|
-
name: string;
|
|
25
|
-
code: string;
|
|
26
|
-
length: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class Authn8Error extends Error {
|
|
30
|
-
constructor(
|
|
31
|
-
message: string,
|
|
32
|
-
public statusCode?: number
|
|
33
|
-
) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.name = "Authn8Error";
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let accountsCache: { accounts: Account[]; timestamp: number } | null = null;
|
|
40
|
-
const CACHE_TTL_MS = 60 * 1000; // 60 seconds
|
|
41
|
-
|
|
42
|
-
async function apiRequest<T>(
|
|
43
|
-
endpoint: string,
|
|
44
|
-
method: string = "GET"
|
|
45
|
-
): Promise<T> {
|
|
46
|
-
if (!API_KEY) {
|
|
47
|
-
throw new Authn8Error(
|
|
48
|
-
"AUTHN8_API_KEY environment variable is not set. Please set it to your PAT token from the Authn8 dashboard."
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const url = `${API_URL}${endpoint}`;
|
|
53
|
-
|
|
54
|
-
const response = await fetch(url, {
|
|
55
|
-
method,
|
|
56
|
-
headers: {
|
|
57
|
-
Authorization: `Bearer ${API_KEY}`,
|
|
58
|
-
"User-Agent": USER_AGENT,
|
|
59
|
-
"Content-Type": "application/json",
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (!response.ok) {
|
|
64
|
-
if (response.status === 401) {
|
|
65
|
-
throw new Authn8Error(
|
|
66
|
-
"Token is invalid or expired. Please check your token in the Authn8 dashboard.",
|
|
67
|
-
401
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (response.status === 403) {
|
|
71
|
-
throw new Authn8Error(
|
|
72
|
-
"Token does not have permission to access this resource.",
|
|
73
|
-
403
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (response.status === 404) {
|
|
77
|
-
throw new Authn8Error("Resource not found.", 404);
|
|
78
|
-
}
|
|
79
|
-
if (response.status === 429) {
|
|
80
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
81
|
-
throw new Authn8Error(
|
|
82
|
-
`Rate limited. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Please try again later."}`,
|
|
83
|
-
429
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
throw new Authn8Error(
|
|
87
|
-
`API request failed with status ${response.status}`,
|
|
88
|
-
response.status
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return response.json() as Promise<T>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function getTokenInfo(): Promise<TokenInfo> {
|
|
96
|
-
return apiRequest<TokenInfo>("/api/pat/me");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function listAccounts(): Promise<Account[]> {
|
|
100
|
-
// Check cache
|
|
101
|
-
if (accountsCache && Date.now() - accountsCache.timestamp < CACHE_TTL_MS) {
|
|
102
|
-
return accountsCache.accounts;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const response = await apiRequest<AccountsResponse>("/api/pat/accounts");
|
|
106
|
-
|
|
107
|
-
// Update cache
|
|
108
|
-
accountsCache = {
|
|
109
|
-
accounts: response.accounts,
|
|
110
|
-
timestamp: Date.now(),
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
return response.accounts;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function getOtp(accountId: string): Promise<OtpResponse> {
|
|
117
|
-
return apiRequest<OtpResponse>(`/api/pat/otp/${accountId}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export async function findAccountByName(
|
|
121
|
-
name: string
|
|
122
|
-
): Promise<{ account: Account } | { matches: Account[] }> {
|
|
123
|
-
const accounts = await listAccounts();
|
|
124
|
-
const lowerName = name.toLowerCase();
|
|
125
|
-
|
|
126
|
-
const matches = accounts.filter(
|
|
127
|
-
(acc) =>
|
|
128
|
-
acc.name?.toLowerCase().includes(lowerName) ||
|
|
129
|
-
acc.issuerDomain?.toLowerCase().includes(lowerName)
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
if (matches.length === 0) {
|
|
133
|
-
throw new Authn8Error(
|
|
134
|
-
`No account found matching "${name}". Available accounts:\n${accounts.map((a) => ` - ${a.name} (${a.issuerDomain || "unknown"})`).join("\n")}`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (matches.length === 1) {
|
|
139
|
-
return { account: matches[0] };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { matches };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function validateToken(): Promise<TokenInfo> {
|
|
146
|
-
try {
|
|
147
|
-
return await getTokenInfo();
|
|
148
|
-
} catch (error) {
|
|
149
|
-
if (error instanceof Authn8Error) {
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
throw new Authn8Error(
|
|
153
|
-
`Failed to connect to Authn8 API. Please check AUTHN8_API_URL (${API_URL}). Error: ${error instanceof Error ? error.message : String(error)}`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { validateToken, type TokenInfo } from "./client.js";
|
|
4
|
-
import {
|
|
5
|
-
toolDefinitions,
|
|
6
|
-
handleListAccounts,
|
|
7
|
-
handleGetOtp,
|
|
8
|
-
handleWhoami,
|
|
9
|
-
} from "./tools.js";
|
|
10
|
-
|
|
11
|
-
function formatDate(isoDate: string): string {
|
|
12
|
-
const date = new Date(isoDate);
|
|
13
|
-
return date.toLocaleDateString("en-US", {
|
|
14
|
-
month: "short",
|
|
15
|
-
day: "numeric",
|
|
16
|
-
year: "numeric",
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function displayStartupInfo(info: TokenInfo): void {
|
|
21
|
-
console.error("Authn8 MCP Server");
|
|
22
|
-
console.error(`Business: ${info.businessName}`);
|
|
23
|
-
console.error(`Token: ${info.tokenName}`);
|
|
24
|
-
console.error(`Accounts: ${info.accountCount}`);
|
|
25
|
-
console.error(`Expires: ${formatDate(info.expiresAt)}`);
|
|
26
|
-
console.error("");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function main(): Promise<void> {
|
|
30
|
-
// Validate token on startup
|
|
31
|
-
let tokenInfo: TokenInfo;
|
|
32
|
-
try {
|
|
33
|
-
tokenInfo = await validateToken();
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error(
|
|
36
|
-
"Error:",
|
|
37
|
-
error instanceof Error ? error.message : String(error)
|
|
38
|
-
);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
displayStartupInfo(tokenInfo);
|
|
43
|
-
|
|
44
|
-
// Create MCP server
|
|
45
|
-
const server = new McpServer({
|
|
46
|
-
name: "authn8-mcp",
|
|
47
|
-
version: "1.0.0",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Register tools
|
|
51
|
-
server.registerTool(
|
|
52
|
-
"list_accounts",
|
|
53
|
-
{
|
|
54
|
-
description: toolDefinitions.list_accounts.description,
|
|
55
|
-
inputSchema: toolDefinitions.list_accounts.inputSchema,
|
|
56
|
-
},
|
|
57
|
-
async () => {
|
|
58
|
-
try {
|
|
59
|
-
return await handleListAccounts();
|
|
60
|
-
} catch (error) {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
type: "text" as const,
|
|
65
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
server.registerTool(
|
|
74
|
-
"get_otp",
|
|
75
|
-
{
|
|
76
|
-
description: toolDefinitions.get_otp.description,
|
|
77
|
-
inputSchema: toolDefinitions.get_otp.inputSchema,
|
|
78
|
-
},
|
|
79
|
-
async (args) => {
|
|
80
|
-
try {
|
|
81
|
-
return await handleGetOtp(args as { account_id?: string; account_name?: string });
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return {
|
|
84
|
-
content: [
|
|
85
|
-
{
|
|
86
|
-
type: "text" as const,
|
|
87
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
server.registerTool(
|
|
96
|
-
"whoami",
|
|
97
|
-
{
|
|
98
|
-
description: toolDefinitions.whoami.description,
|
|
99
|
-
inputSchema: toolDefinitions.whoami.inputSchema,
|
|
100
|
-
},
|
|
101
|
-
async () => {
|
|
102
|
-
try {
|
|
103
|
-
return await handleWhoami();
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return {
|
|
106
|
-
content: [
|
|
107
|
-
{
|
|
108
|
-
type: "text" as const,
|
|
109
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Connect via stdio transport
|
|
118
|
-
const transport = new StdioServerTransport();
|
|
119
|
-
await server.connect(transport);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
main().catch((error) => {
|
|
123
|
-
console.error("Fatal error:", error);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
});
|
package/src/tools.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
getTokenInfo,
|
|
4
|
-
listAccounts,
|
|
5
|
-
getOtp,
|
|
6
|
-
findAccountByName,
|
|
7
|
-
type TokenInfo,
|
|
8
|
-
type Account,
|
|
9
|
-
type OtpResponse,
|
|
10
|
-
} from "./client.js";
|
|
11
|
-
|
|
12
|
-
export const toolDefinitions = {
|
|
13
|
-
list_accounts: {
|
|
14
|
-
description:
|
|
15
|
-
"Returns all 2FA accounts accessible to this token. Use this to see what accounts are available before requesting an OTP code.",
|
|
16
|
-
inputSchema: z.object({}),
|
|
17
|
-
},
|
|
18
|
-
get_otp: {
|
|
19
|
-
description:
|
|
20
|
-
"Generates a TOTP code for a specific account. You can provide either the account_id (UUID) or account_name (partial match supported). If multiple accounts match the name, you'll get a list of matches to be more specific.",
|
|
21
|
-
inputSchema: z.object({
|
|
22
|
-
account_id: z
|
|
23
|
-
.uuid()
|
|
24
|
-
.optional()
|
|
25
|
-
.describe("UUID of the account"),
|
|
26
|
-
account_name: z
|
|
27
|
-
.string()
|
|
28
|
-
.optional()
|
|
29
|
-
.describe("Name to search for (partial match)"),
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
whoami: {
|
|
33
|
-
description:
|
|
34
|
-
"Returns information about the current token and what it has access to, including the business name, token name, scoped groups, account count, and expiration date.",
|
|
35
|
-
inputSchema: z.object({}),
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export async function handleListAccounts(): Promise<{
|
|
40
|
-
content: Array<{ type: "text"; text: string }>;
|
|
41
|
-
}> {
|
|
42
|
-
const accounts = await listAccounts();
|
|
43
|
-
|
|
44
|
-
if (accounts.length === 0) {
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: "No accounts are accessible with this token.",
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const formatted = accounts.map((acc) => ({
|
|
56
|
-
id: acc.id,
|
|
57
|
-
name: acc.name,
|
|
58
|
-
issuer: acc.issuerDomain || null,
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
type: "text",
|
|
65
|
-
text: JSON.stringify(formatted, null, 2),
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function handleGetOtp(args: {
|
|
72
|
-
account_id?: string;
|
|
73
|
-
account_name?: string;
|
|
74
|
-
}): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
75
|
-
const { account_id, account_name } = args;
|
|
76
|
-
|
|
77
|
-
if (!account_id && !account_name) {
|
|
78
|
-
return {
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: "text",
|
|
82
|
-
text: "Error: Either account_id or account_name must be provided.",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let targetAccountId: string;
|
|
89
|
-
let accountName: string;
|
|
90
|
-
|
|
91
|
-
if (account_id) {
|
|
92
|
-
targetAccountId = account_id;
|
|
93
|
-
// Get the account name for the response
|
|
94
|
-
const accounts = await listAccounts();
|
|
95
|
-
const account = accounts.find((a) => a.id === account_id);
|
|
96
|
-
if (!account) {
|
|
97
|
-
return {
|
|
98
|
-
content: [
|
|
99
|
-
{
|
|
100
|
-
type: "text",
|
|
101
|
-
text: `Error: Account with ID "${account_id}" not found. Use list_accounts to see available accounts.`,
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
accountName = account.name;
|
|
107
|
-
} else {
|
|
108
|
-
// Search by name
|
|
109
|
-
const result = await findAccountByName(account_name!);
|
|
110
|
-
|
|
111
|
-
if ("matches" in result) {
|
|
112
|
-
return {
|
|
113
|
-
content: [
|
|
114
|
-
{
|
|
115
|
-
type: "text",
|
|
116
|
-
text: `Multiple accounts match "${account_name}". Please be more specific:\n${result.matches.map((a) => ` - ${a.name} (${a.issuerDomain || "unknown"}) - ID: ${a.id}`).join("\n")}`,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
targetAccountId = result.account.id;
|
|
123
|
-
accountName = result.account.name;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const otp = await getOtp(targetAccountId);
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
content: [
|
|
130
|
-
{
|
|
131
|
-
type: "text",
|
|
132
|
-
text: JSON.stringify(
|
|
133
|
-
{
|
|
134
|
-
account: accountName,
|
|
135
|
-
code: otp.code,
|
|
136
|
-
},
|
|
137
|
-
null,
|
|
138
|
-
2
|
|
139
|
-
),
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function handleWhoami(): Promise<{
|
|
146
|
-
content: Array<{ type: "text"; text: string }>;
|
|
147
|
-
}> {
|
|
148
|
-
const info = await getTokenInfo();
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
content: [
|
|
152
|
-
{
|
|
153
|
-
type: "text",
|
|
154
|
-
text: JSON.stringify(
|
|
155
|
-
{
|
|
156
|
-
business: info.businessName,
|
|
157
|
-
token_name: info.tokenName,
|
|
158
|
-
scoped_groups: info.scopedGroups.map((g) => g.name),
|
|
159
|
-
account_count: info.accountCount,
|
|
160
|
-
expires_at: info.expiresAt,
|
|
161
|
-
},
|
|
162
|
-
null,
|
|
163
|
-
2
|
|
164
|
-
),
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
};
|
|
168
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"outDir": "./dist",
|
|
10
|
-
"rootDir": "./src",
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"resolveJsonModule": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist"]
|
|
16
|
-
}
|