Skip to content

Commit 9aacc91

Browse files
patch: Remove file-based token cache (#193)
1 parent ca2ae23 commit 9aacc91

File tree

7 files changed

+23
-88
lines changed

7 files changed

+23
-88
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Unreleased Changes
44

5-
_No unreleased changes yet._
5+
## 0.9.1
6+
7+
- Replaced file-based token cache with an in-memory cache to avoid writing credentials to disk. Tokens now reset on server restart.
68

79
## 0.9.0
810

gemini-extension.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dynatrace-mcp-server",
3-
"version": "0.9.0",
3+
"version": "0.9.1",
44
"mcpServers": {
55
"dynatrace": {
66
"command": "npx",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dynatrace-oss/dynatrace-mcp-server",
3-
"version": "0.9.0",
3+
"version": "0.9.1",
44
"mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
55
"description": "Model Context Protocol (MCP) server for Dynatrace",
66
"keywords": [

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
"url": "https://github.com/dynatrace-oss/Dynatrace-mcp",
88
"source": "github"
99
},
10-
"version": "0.9.0",
10+
"version": "0.9.1",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"registryBaseUrl": "https://registry.npmjs.org",
1515
"identifier": "@dynatrace-oss/dynatrace-mcp-server",
16-
"version": "0.9.0",
16+
"version": "0.9.1",
1717
"runtimeHint": "npx",
1818
"transport": {
1919
"type": "stdio"

src/authentication/dynatrace-clients.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ const createOAuthClientCredentialsHttpClient = async (
103103

104104
/** Create an OAuth Client using authorization code flow (interactive authentication)
105105
* This starts a local HTTP server to handle the OAuth redirect and requires user interaction.
106-
* Implements token caching (via .dt-mcp/token.json) to avoid repeated OAuth flows.
106+
* Implements an in-memory token cache (not persisted to disk). After every server restart a new
107+
* authentication flow (or token refresh) may be required.
107108
* Note: Always requests a complete set of scopes for maximum token reusability. Else the user will end up having to approve multiple requests.
108109
*/
109110
const createOAuthAuthCodeFlowHttpClient = async (

src/authentication/token-cache.ts

Lines changed: 12 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,56 @@
11
import { CachedToken, TokenCache, OAuthTokenResponse } from './types';
2-
import * as fs from 'fs';
3-
import * as path from 'path';
42

53
/**
6-
* File-based token cache implementation that persists tokens to disk
7-
* Stores tokens in .dt-mcp/token.json for persistence across dynatrace-mcp-server restarts
4+
* In-memory token cache implementation (no persistence across process restarts).
5+
* The previous implementation stored tokens on disk in `.dt-mcp/token.json` – this has been
6+
* intentionally removed to avoid writing credentials to the local filesystem. A new login /
7+
* OAuth authorization code flow (or token retrieval) will be required after every server restart.
88
*/
9-
export class FileTokenCache implements TokenCache {
10-
private readonly tokenFilePath: string;
9+
export class InMemoryTokenCache implements TokenCache {
1110
private token: CachedToken | null = null;
1211

13-
constructor() {
14-
// Create .dt-mcp directory in the current working directory
15-
const tokenDir = path.join(process.cwd(), '.dt-mcp');
16-
this.tokenFilePath = path.join(tokenDir, 'token.json');
17-
18-
// Ensure the directory exists
19-
if (!fs.existsSync(tokenDir)) {
20-
fs.mkdirSync(tokenDir, { recursive: true });
21-
}
22-
23-
this.loadToken();
24-
}
25-
26-
/**
27-
* Loads the token from the file system
28-
*/
29-
private loadToken(): void {
30-
try {
31-
if (fs.existsSync(this.tokenFilePath)) {
32-
const tokenData = fs.readFileSync(this.tokenFilePath, 'utf8');
33-
this.token = JSON.parse(tokenData);
34-
console.error(`🔍 Loaded token from file: ${this.tokenFilePath}`);
35-
} else {
36-
console.error(`🔍 No token file found at: ${this.tokenFilePath}`);
37-
this.token = null;
38-
}
39-
} catch (error) {
40-
console.error(`❌ Failed to load token from file: ${error}`);
41-
this.token = null;
42-
}
43-
}
44-
45-
/**
46-
* Saves the token to the file system
47-
*/
48-
private saveToken(): void {
49-
try {
50-
if (this.token) {
51-
fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.token, null, 2), 'utf8');
52-
console.error(`✅ Saved token to file: ${this.tokenFilePath}`);
53-
} else {
54-
// Remove the file if no token exists
55-
if (fs.existsSync(this.tokenFilePath)) {
56-
fs.unlinkSync(this.tokenFilePath);
57-
console.error(`🗑️ Removed token file: ${this.tokenFilePath}`);
58-
}
59-
}
60-
} catch (error) {
61-
console.error(`❌ Failed to save token to file: ${error}`);
62-
}
63-
}
64-
6512
/**
6613
* Retrieves the cached token (ignores scopes since we use a global token)
6714
*/
6815
getToken(scopes: string[]): CachedToken | null {
69-
// We ignore the scopes parameter since we use a single token with all scopes
16+
// Scopes parameter ignored – single global token covers all requested scopes.
7017
return this.token;
7118
}
7219

7320
/**
7421
* Stores the global token in the cache and persists it to file
7522
*/
7623
setToken(scopes: string[], token: OAuthTokenResponse): void {
77-
// We ignore the scopes parameter since we use a single token with all scopes
7824
this.token = {
7925
access_token: token.access_token!,
8026
refresh_token: token.refresh_token,
8127
expires_at: token.expires_in ? Date.now() + token.expires_in * 1000 : undefined,
82-
scopes: [...scopes], // Store the actual scopes that were granted
28+
scopes: [...scopes],
8329
};
84-
85-
this.saveToken();
8630
}
8731

8832
/**
8933
* Removes the cached token and deletes the file
9034
*/
9135
clearToken(scopes?: string[]): void {
92-
// We ignore the scopes parameter since we use a single global token
9336
this.token = null;
94-
this.saveToken();
9537
}
9638

9739
/**
9840
* Checks if the token exists and is still valid (not expired)
9941
*/
10042
isTokenValid(scopes: string[]): boolean {
10143
// We ignore the scopes parameter since we use a single token with all scopes
102-
if (!this.token) {
103-
console.error(`🔍 Token validation: No token in cache`);
104-
return false;
105-
}
106-
107-
// If no expiration time is set, assume token is valid
108-
if (!this.token.expires_at) {
109-
console.error(`🔍 Token validation: Token has no expiration, assuming valid`);
110-
return true;
111-
}
44+
if (!this.token) return false;
45+
if (!this.token.expires_at) return true; // treat as non-expiring
11246

11347
// Add a 30-second buffer to avoid using tokens that are about to expire
11448
const bufferMs = 30 * 1000; // 30 seconds
11549
const now = Date.now();
11650
const expiresAt = this.token.expires_at;
117-
const isValid = now + bufferMs < expiresAt;
118-
119-
return isValid;
51+
return now + bufferMs < expiresAt;
12052
}
12153
}
12254

123-
// Global token cache instance - uses file-based persistence
124-
export const globalTokenCache = new FileTokenCache();
55+
// Global token cache instance - In-memory only
56+
export const globalTokenCache = new InMemoryTokenCache();

0 commit comments

Comments
 (0)