forked from nicobailon/pi-mcp-adapter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcp-auth.ts
More file actions
277 lines (248 loc) · 7.04 KB
/
mcp-auth.ts
File metadata and controls
277 lines (248 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
* MCP Auth Storage Module
*
* Handles secure storage of OAuth credentials, tokens, client information,
* and PKCE state for MCP servers. Maintains backward compatibility with
* per-server directory structure.
*
* Token storage location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
*/
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
/** OAuth token storage format */
export interface StoredTokens {
accessToken: string;
refreshToken?: string;
expiresAt?: number; // Unix timestamp in seconds
scope?: string;
}
/** OAuth client information from dynamic or static registration */
export interface StoredClientInfo {
clientId: string;
clientSecret?: string;
clientIdIssuedAt?: number;
clientSecretExpiresAt?: number;
}
/** Complete auth entry for a server */
export interface AuthEntry {
tokens?: StoredTokens;
clientInfo?: StoredClientInfo;
codeVerifier?: string;
oauthState?: string;
serverUrl?: string; // Track the URL these credentials are for
}
// Base directory for auth storage - can be overridden via env var for testing
function getAuthBaseDir(): string {
return process.env.MCP_OAUTH_DIR
? process.env.MCP_OAUTH_DIR
: join(homedir(), '.pi', 'agent', 'mcp-oauth');
}
/**
* Get the server-specific directory path.
*/
function getServerDir(serverName: string): string {
return join(getAuthBaseDir(), serverName);
}
/**
* Get the tokens file path for a server.
*/
function getTokensFilePath(serverName: string): string {
return join(getServerDir(serverName), 'tokens.json');
}
/**
* Ensure the server directory exists with secure permissions.
*/
function ensureServerDir(serverName: string): void {
const dir = getServerDir(serverName);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
}
}
/**
* Read the auth entry for a server from disk.
* Returns undefined if file doesn't exist.
*/
function readAuthEntry(serverName: string): AuthEntry | undefined {
try {
const filePath = getTokensFilePath(serverName);
if (!existsSync(filePath)) {
return undefined;
}
const data = readFileSync(filePath, 'utf-8');
return JSON.parse(data) as AuthEntry;
} catch (error) {
console.error(`Failed to read auth entry for ${serverName}:`, error);
return undefined;
}
}
/**
* Write the auth entry for a server to disk with secure permissions.
*/
function writeAuthEntry(serverName: string, entry: AuthEntry): void {
ensureServerDir(serverName);
const filePath = getTokensFilePath(serverName);
writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 0o600 });
}
/**
* Get auth entry for a server.
*/
export function getAuthEntry(serverName: string): AuthEntry | undefined {
return readAuthEntry(serverName);
}
/**
* Get auth entry and validate it's for the correct URL.
* Returns undefined if URL has changed (credentials are invalid).
*/
export function getAuthForUrl(serverName: string, serverUrl: string): AuthEntry | undefined {
const entry = getAuthEntry(serverName);
if (!entry) return undefined;
// If no serverUrl is stored, this is from an old version - consider it invalid
if (!entry.serverUrl) return undefined;
// If URL has changed, credentials are invalid
if (entry.serverUrl !== serverUrl) return undefined;
return entry;
}
/**
* Save auth entry for a server.
*/
export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?: string): void {
// Always update serverUrl if provided
if (serverUrl) {
entry.serverUrl = serverUrl;
}
writeAuthEntry(serverName, entry);
}
/**
* Remove auth entry for a server.
* Also removes the server directory if empty.
*/
export function removeAuthEntry(serverName: string): void {
try {
const filePath = getTokensFilePath(serverName);
if (existsSync(filePath)) {
writeFileSync(filePath, '{}', { mode: 0o600 });
}
// Try to remove the directory
const dir = getServerDir(serverName);
if (existsSync(dir)) {
try {
rmSync(dir, { recursive: true });
} catch {
// Directory may not be empty, ignore
}
}
} catch (error) {
console.error(`Failed to remove auth entry for ${serverName}:`, error);
}
}
/**
* Update tokens for a server.
*/
export function updateTokens(
serverName: string,
tokens: StoredTokens,
serverUrl?: string
): void {
const entry = getAuthEntry(serverName) ?? {};
entry.tokens = tokens;
saveAuthEntry(serverName, entry, serverUrl);
}
/**
* Update client info for a server.
*/
export function updateClientInfo(
serverName: string,
clientInfo: StoredClientInfo,
serverUrl?: string
): void {
const entry = getAuthEntry(serverName) ?? {};
entry.clientInfo = clientInfo;
saveAuthEntry(serverName, entry, serverUrl);
}
/**
* Update code verifier for a server.
*/
export function updateCodeVerifier(serverName: string, codeVerifier: string): void {
const entry = getAuthEntry(serverName) ?? {};
entry.codeVerifier = codeVerifier;
saveAuthEntry(serverName, entry);
}
/**
* Clear code verifier for a server.
*/
export function clearCodeVerifier(serverName: string): void {
const entry = getAuthEntry(serverName);
if (entry) {
delete entry.codeVerifier;
saveAuthEntry(serverName, entry);
}
}
/**
* Update OAuth state for a server.
*/
export function updateOAuthState(serverName: string, state: string): void {
const entry = getAuthEntry(serverName) ?? {};
entry.oauthState = state;
saveAuthEntry(serverName, entry);
}
/**
* Get OAuth state for a server.
*/
export function getOAuthState(serverName: string): string | undefined {
const entry = getAuthEntry(serverName);
return entry?.oauthState;
}
/**
* Clear OAuth state for a server.
*/
export function clearOAuthState(serverName: string): void {
const entry = getAuthEntry(serverName);
if (entry) {
delete entry.oauthState;
saveAuthEntry(serverName, entry);
}
}
/**
* Check if stored tokens are expired.
* Returns null if no tokens exist, false if no expiry or not expired, true if expired.
*/
export function isTokenExpired(serverName: string): boolean | null {
const entry = getAuthEntry(serverName);
if (!entry?.tokens) return null;
if (!entry.tokens.expiresAt) return false;
return entry.tokens.expiresAt < Date.now() / 1000;
}
/**
* Check if a server has stored tokens.
*/
export function hasStoredTokens(serverName: string): boolean {
const entry = getAuthEntry(serverName);
return !!entry?.tokens;
}
/**
* Clear all credentials for a server.
*/
export function clearAllCredentials(serverName: string): void {
removeAuthEntry(serverName);
}
/**
* Clear only client info for a server.
*/
export function clearClientInfo(serverName: string): void {
const entry = getAuthEntry(serverName);
if (entry) {
delete entry.clientInfo;
saveAuthEntry(serverName, entry);
}
}
/**
* Clear only tokens for a server.
*/
export function clearTokens(serverName: string): void {
const entry = getAuthEntry(serverName);
if (entry) {
delete entry.tokens;
saveAuthEntry(serverName, entry);
}
}