diff --git a/package-lock.json b/package-lock.json index 55a39725..27c5b68f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1575,9 +1575,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", - "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.7", @@ -18483,7 +18483,7 @@ "dependencies": { "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.7", - "@modelcontextprotocol/sdk": "^1.25.1", + "@modelcontextprotocol/sdk": "^1.25.2", "hono": "^4.11.3", "zod": "^3.25.76" }, diff --git a/plugins-external/mcp-server/package.json b/plugins-external/mcp-server/package.json index f2ff54b4..f9ef3aa4 100644 --- a/plugins-external/mcp-server/package.json +++ b/plugins-external/mcp-server/package.json @@ -17,7 +17,7 @@ "dependencies": { "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.7", - "@modelcontextprotocol/sdk": "^1.25.1", + "@modelcontextprotocol/sdk": "^1.25.2", "hono": "^4.11.3", "zod": "^3.25.76" }, diff --git a/plugins/template-function-1password/src/index.ts b/plugins/template-function-1password/src/index.ts index 9ccff23f..170a2cfd 100644 --- a/plugins/template-function-1password/src/index.ts +++ b/plugins/template-function-1password/src/index.ts @@ -6,7 +6,18 @@ import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; const _clients: Record = {}; -async function op(args: CallTemplateFunctionArgs): Promise<{ client?: Client; error?: unknown }> { +// Cache for API responses to avoid rate limiting +interface CacheEntry { + data: T; + expiresAt: number; +} + +const CACHE_TTL_MS = 60 * 1000; // 1 minute cache TTL +const _cache: Record> = {}; + +async function op( + args: CallTemplateFunctionArgs, +): Promise<{ client?: Client; clientHash?: string; error?: unknown }> { let authMethod: string | DesktopAuth | null = null; let hash: string | null = null; switch (args.values.authMethod) { @@ -40,7 +51,7 @@ async function op(args: CallTemplateFunctionArgs): Promise<{ client?: Client; er return { error: e }; } - return { client: _clients[hash] }; + return { client: _clients[hash], clientHash: hash }; } async function getValue( @@ -49,22 +60,30 @@ async function getValue( itemId?: JsonPrimitive, fieldId?: JsonPrimitive, ): Promise<{ value?: string; error?: unknown }> { - const { client, error } = await op(args); - if (!client) return { error }; + const { client, error, clientHash } = await op(args); + if (!client || !clientHash) return { error }; if (vaultId && typeof vaultId === 'string') { - try { - await client.vaults.getOverview(vaultId); - } catch { - return { error: `Vault ${vaultId} not found` }; + const vaultCacheKey = `${clientHash}:vault:${vaultId}`; + const cachedVault = getCached(vaultCacheKey); + if (!cachedVault) { + try { + const vault = await client.vaults.getOverview(vaultId); + setCache(vaultCacheKey, vault); + } catch { + return { error: `Vault ${vaultId} not found` }; + } } } else { return { error: 'No vault specified' }; } if (itemId && typeof itemId === 'string') { + const itemCacheKey = `${clientHash}:item:${vaultId}:${itemId}`; try { - const item = await client.items.get(vaultId, itemId); + const item = + getCached>>(itemCacheKey) ?? + setCache(itemCacheKey, await client.items.get(vaultId, itemId)); if (fieldId && typeof fieldId === 'string') { const field = item.fields.find((f) => f.id === fieldId); if (field) { @@ -142,10 +161,15 @@ export const plugin: PluginDefinition = { type: 'select', options: [], async dynamic(_ctx, args) { - const { client } = await op(args); - if (client == null) return { hidden: true }; - // Fetches a secret. - const vaults = await client.vaults.list({ decryptDetails: true }); + const { client, clientHash } = await op(args); + if (client == null || clientHash == null) return { hidden: true }; + + const cacheKey = `${clientHash}:vaults`; + const cachedVaults = + getCached>>(cacheKey); + const vaults = + cachedVaults ?? + setCache(cacheKey, await client.vaults.list({ decryptDetails: true })); return { options: vaults.map((vault) => ({ label: `${vault.title} (${vault.activeItemCount} Items)`, @@ -160,13 +184,16 @@ export const plugin: PluginDefinition = { type: 'select', options: [], async dynamic(_ctx, args) { - const { client } = await op(args); - if (client == null) return { hidden: true }; + const { client, clientHash } = await op(args); + if (client == null || clientHash == null) return { hidden: true }; const vaultId = args.values.vault; if (typeof vaultId !== 'string') return { hidden: true }; try { - const items = await client.items.list(vaultId); + const cacheKey = `${clientHash}:items:${vaultId}`; + const cachedItems = + getCached>>(cacheKey); + const items = cachedItems ?? setCache(cacheKey, await client.items.list(vaultId)); return { options: items.map((item) => ({ label: `${item.title} ${item.category}`, @@ -185,8 +212,8 @@ export const plugin: PluginDefinition = { type: 'select', options: [], async dynamic(_ctx, args) { - const { client } = await op(args); - if (client == null) return { hidden: true }; + const { client, clientHash } = await op(args); + if (client == null || clientHash == null) return { hidden: true }; const vaultId = args.values.vault; const itemId = args.values.item; if (typeof vaultId !== 'string' || typeof itemId !== 'string') { @@ -194,7 +221,10 @@ export const plugin: PluginDefinition = { } try { - const item = await client.items.get(vaultId, itemId); + const cacheKey = `${clientHash}:item:${vaultId}:${itemId}`; + const cachedItem = getCached>>(cacheKey); + const item = + cachedItem ?? setCache(cacheKey, await client.items.get(vaultId, itemId)); return { options: item.fields.map((field) => ({ label: field.title, value: field.id })), }; @@ -219,3 +249,23 @@ export const plugin: PluginDefinition = { }, ], }; + +function getCached(key: string): T | undefined { + const entry = _cache[key]; + if (entry && entry.expiresAt > Date.now()) { + return entry.data as T; + } + // Clean up expired entry + if (entry) { + delete _cache[key]; + } + return undefined; +} + +function setCache(key: string, data: T): T { + _cache[key] = { + data, + expiresAt: Date.now() + CACHE_TTL_MS, + }; + return data; +}