@authn8/mcp-server 1.0.5 → 1.1.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 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
- "id": "924c52a6-4457-4970-a39f-4dc620217683",
104
- "name": "AWS Production",
105
- "issuer": "amazon.com"
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 name, the tool returns a list of matches.
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
- "account": "AWS Production",
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
- "business": "Bytecode Solutions",
136
- "token_name": "MCP Server 2",
137
- "scoped_groups": ["HR"],
138
- "account_count": 1,
139
- "expires_at": "2025-12-25T23:59:59Z"
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 = "authn8-mcp/1.0.0";
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
- async function findAccountByName(name) {
83
- const accounts = await listAccounts();
89
+ function matchAccounts(accounts, name) {
84
90
  const lowerName = name.toLowerCase();
85
- const matches = accounts.filter(
91
+ return accounts.filter(
86
92
  (acc) => acc.name?.toLowerCase().includes(lowerName) || acc.issuerDomain?.toLowerCase().includes(lowerName)
87
93
  );
88
- if (matches.length === 0) {
89
- throw new Authn8Error(
90
- `No account found matching "${name}". Available accounts:
91
- ${accounts.map((a) => ` - ${a.name} (${a.issuerDomain || "unknown"})`).join("\n")}`
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 { matches };
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. 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.",
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("UUID of the account"),
123
- account_name: z.string().optional().describe("Name to search for (partial match)")
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
- async function handleListAccounts() {
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
- content: [
162
- {
163
- type: "text",
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 result = await findAccountByName(account_name);
188
- if ("matches" in result) {
189
- return {
190
- content: [
191
- {
192
- type: "text",
193
- text: `Multiple accounts match "${account_name}". Please be more specific:
194
- ${result.matches.map((a) => ` - ${a.name} (${a.issuerDomain || "unknown"}) - ID: ${a.id}`).join("\n")}`
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 = result.account.id;
200
- accountName = result.account.name;
212
+ targetAccountId = matches[0].id;
201
213
  }
202
214
  const otp = await getOtp(targetAccountId);
203
- return {
204
- content: [
205
- {
206
- type: "text",
207
- text: JSON.stringify(
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
- content: [
223
- {
224
- type: "text",
225
- text: JSON.stringify(
226
- {
227
- business: info.businessName,
228
- token_name: info.tokenName,
229
- scoped_groups: info.scopedGroups.map((g) => g.name),
230
- account_count: info.accountCount,
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("Authn8 MCP Server");
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: "1.0.0"
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(args);
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,18 @@
1
1
  {
2
2
  "name": "@authn8/mcp-server",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Authn8 2FA code access",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "authn8-mcp": "./dist/index.js"
8
8
  },
9
9
  "main": "dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
10
13
  "scripts": {
11
14
  "build": "tsup src/index.ts --format esm --dts",
12
- "dev": "node --loader ts-node/esm src/index.ts",
15
+ "dev": "tsup src/index.ts --format esm --watch",
13
16
  "start": "node dist/index.js"
14
17
  },
15
18
  "keywords": [
@@ -21,13 +24,13 @@
21
24
  ],
22
25
  "repository": {
23
26
  "type": "git",
24
- "url": "https://github.com/authn8/mcp-server"
27
+ "url": "git+https://github.com/authn8/mcp-server.git"
25
28
  },
26
29
  "homepage": "https://authn8.com",
27
- "author": "",
28
- "license": "ISC",
30
+ "author": "Authn8",
31
+ "license": "MIT",
29
32
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.24.3",
33
+ "@modelcontextprotocol/sdk": "^1.30.0",
31
34
  "zod": "^4.1.13"
32
35
  },
33
36
  "devDependencies": {
package/Dockerfile DELETED
@@ -1,6 +0,0 @@
1
- FROM node:22-alpine
2
- WORKDIR /app
3
- COPY package*.json ./
4
- RUN npm ci --only=production
5
- COPY dist/ ./dist/
6
- ENTRYPOINT ["node", "dist/index.js"]
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
- }
package/tsup.config.ts DELETED
@@ -1,12 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts"],
5
- format: ["esm"],
6
- dts: true,
7
- clean: true,
8
- shims: true,
9
- banner: {
10
- js: "#!/usr/bin/env node",
11
- },
12
- });