Skip to main content
Glama
secrets.ts7.12 kB
/** * Secrets namespace - Key-value storage with versioning */ import fs from 'fs'; import path from 'path'; import os from 'os'; import crypto from 'crypto'; import { MCPServer } from '../core/server.js'; import { NotFoundError, InvalidArgError } from '../core/errors.js'; import { Secret, SecretMeta, PutSecretResponse, GetSecretResponse, SecretListItem, ListSecretsResponse } from '../types/secrets.js'; export class SecretsNamespace { private mcpServer: MCPServer; private secrets = new Map<string, Secret>(); private secretsFile: string; private encryptionKey: Buffer; constructor(mcpServer: MCPServer) { this.mcpServer = mcpServer; // Initialize secrets storage const secretsDir = path.join(os.homedir(), '.mcp-fullstack', 'secrets'); if (!fs.existsSync(secretsDir)) { fs.mkdirSync(secretsDir, { recursive: true }); } this.secretsFile = path.join(secretsDir, 'secrets.json'); // Generate or load encryption key const keyFile = path.join(secretsDir, '.key'); if (fs.existsSync(keyFile)) { this.encryptionKey = fs.readFileSync(keyFile); } else { this.encryptionKey = crypto.randomBytes(32); fs.writeFileSync(keyFile, this.encryptionKey, { mode: 0o600 }); } this.loadSecrets(); this.registerTools(); } private loadSecrets(): void { if (fs.existsSync(this.secretsFile)) { try { const encryptedData = fs.readFileSync(this.secretsFile, 'utf-8'); const decryptedData = this.decrypt(encryptedData); const data = JSON.parse(decryptedData); for (const [key, secret] of Object.entries(data)) { this.secrets.set(key, secret as Secret); } } catch (error) { console.error('Failed to load secrets:', error); // Initialize with empty secrets if loading fails } } } private saveSecrets(): void { const data: Record<string, Secret> = {}; for (const [key, secret] of this.secrets) { // Never persist the actual value in logs data[key] = secret; } const jsonData = JSON.stringify(data, null, 2); const encryptedData = this.encrypt(jsonData); fs.writeFileSync(this.secretsFile, encryptedData, { mode: 0o600 }); } private encrypt(text: string): string { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } private decrypt(text: string): string { const parts = text.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encryptedText = parts[1]; const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } private registerTools(): void { const registry = this.mcpServer.getRegistry(); registry.registerTool( 'secrets.put', { name: 'secrets.put', description: 'Store a secret value', inputSchema: { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' }, meta: { type: 'object', properties: { description: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } } } } }, required: ['key', 'value'] } }, this.put.bind(this) ); registry.registerTool( 'secrets.get', { name: 'secrets.get', description: 'Retrieve a secret value', inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] } }, this.get.bind(this) ); registry.registerTool( 'secrets.list', { name: 'secrets.list', description: 'List secret keys', inputSchema: { type: 'object', properties: { prefix: { type: 'string' }, limit: { type: 'number' }, cursor: { type: 'string' } } } }, this.list.bind(this) ); registry.registerTool( 'secrets.delete', { name: 'secrets.delete', description: 'Delete a secret', inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] } }, this.delete.bind(this) ); } private async put(params: { key: string; value: string; meta?: SecretMeta; }): Promise<PutSecretResponse> { if (!params.key || params.key.trim() === '') { throw new InvalidArgError('key', 'Key cannot be empty'); } const existing = this.secrets.get(params.key); const version = existing ? existing.version + 1 : 1; const secret: Secret = { key: params.key, value: params.value, version, meta: params.meta, updated_at: new Date().toISOString() }; this.secrets.set(params.key, secret); this.saveSecrets(); // Log the operation but never the value console.log(`Secret stored: key="${params.key}", version=${version}`); return { ok: true, version }; } private async get(params: { key: string; }): Promise<GetSecretResponse> { const secret = this.secrets.get(params.key); if (!secret) { throw new NotFoundError('Secret', params.key); } // Log access but never the value console.log(`Secret accessed: key="${params.key}"`); return { key: secret.key, value: secret.value, version: secret.version, meta: secret.meta }; } private async list(params: { prefix?: string; limit?: number; cursor?: string; }): Promise<ListSecretsResponse> { let keys = Array.from(this.secrets.entries()); // Filter by prefix if (params.prefix) { keys = keys.filter(([key]) => key.startsWith(params.prefix!)); } // Sort by key keys.sort(([a], [b]) => a.localeCompare(b)); // Apply pagination const limit = params.limit || 100; const startIdx = params.cursor ? parseInt(params.cursor) : 0; const paginatedKeys = keys.slice(startIdx, startIdx + limit); const hasMore = startIdx + limit < keys.length; return { keys: paginatedKeys.map(([key, secret]) => ({ key, version: secret.version, updated_at: secret.updated_at })), next_cursor: hasMore ? String(startIdx + limit) : undefined }; } private async delete(params: { key: string; }): Promise<{ ok: true }> { if (!this.secrets.has(params.key)) { throw new NotFoundError('Secret', params.key); } this.secrets.delete(params.key); this.saveSecrets(); console.log(`Secret deleted: key="${params.key}"`); return { ok: true }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JacobFV/mcp-fullstack'

If you have feedback or need assistance with the MCP directory API, please join our Discord server