Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/mcp/src/server/tools/fetch-graph-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export function register(deps: ToolDeps) {
description: "Fetch documents with memories for graph display",
inputSchema: z.object({
containerTag: optionalContainerTagSchema,
page: z.number().optional().default(1),
limit: z.number().optional().default(200),
page: z.number().int().min(1).max(10_000).optional().default(1),
limit: z.number().int().min(1).max(1_000).optional().default(200),
}),
outputSchema: documentsApiResponseSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,
Expand Down
10 changes: 10 additions & 0 deletions apps/mcp/src/server/tools/get-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "./output-schemas"
import { textContent, type ToolDeps } from "./types"

// An out-of-space document reports "not found" on purpose, so the id is not an existence oracle.
export function register(deps: ToolDeps) {
const inputSchema = z.object({
documentId: z
Expand All @@ -28,8 +29,17 @@ export function register(deps: ToolDeps) {
},
async (args) => {
try {
const effectiveTag = await deps.resolveContainerTag()
const client = deps.getClient()
const document = await client.getDocument(args.documentId)
const docTags = document.containerTags
if (
Array.isArray(docTags) &&
docTags.length > 0 &&
!docTags.includes(effectiveTag)
) {
throw new Error("Document not found")
}
const { content, truncated } = getDocumentContent(document)
const structuredContent: GetDocumentOutput = {
document: {
Expand Down
6 changes: 5 additions & 1 deletion apps/mcp/src/server/tools/guided-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export function register(deps: ToolDeps) {
description:
"Open an interactive form when the user wants to draft, review, edit, or choose the target space before saving information to Supermemory. Use this when the user wants to add a memory but has not supplied final content, or explicitly wants to review supplied content before saving. If the user provides the exact content and asks to save it immediately, use add_memory instead.",
inputSchema: z.object({
prefill: z.string().optional().describe("Optional content to prefill"),
prefill: z
.string()
.max(200000, "Prefill exceeds maximum length")
.optional()
.describe("Optional content to prefill"),
}),
outputSchema: saveViewSchema,
_meta: appToolMeta(),
Expand Down
1 change: 0 additions & 1 deletion apps/mcp/src/server/tools/output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ export const whoAmIOutputSchema = z.object({
version: z.string().optional(),
})
.optional(),
sessionId: z.string().optional(),
})

export type WhoAmIOutput = z.infer<typeof whoAmIOutputSchema>
2 changes: 0 additions & 2 deletions apps/mcp/src/server/tools/who-am-i.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function register(deps: ToolDeps) {
deps.getActiveContainerTag(),
])
const client = deps.getClientInfo(context)
const sessionId = context.sessionId
const structuredContent: WhoAmIOutput = {
userId: session.user.id,
...(session.user.email ? { email: session.user.email } : {}),
Expand All @@ -34,7 +33,6 @@ export function register(deps: ToolDeps) {
: null,
...(session.scope ? { scope: session.scope } : {}),
...(client ? { client } : {}),
...(sessionId ? { sessionId } : {}),
}
return {
content: [textContent(JSON.stringify(structuredContent))],
Expand Down
11 changes: 10 additions & 1 deletion apps/raycast-extension/src/search-memories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ const extractContent = (memory: SearchResult) => {
return "No content available"
}

// metadata.url comes from ingested content, so only http(s) reaches the OS opener.
const extractUrl = (memory: SearchResult) => {
if (memory.metadata?.url && typeof memory.metadata.url === "string") {
return memory.metadata.url
const url = memory.metadata.url
try {
const parsed = new URL(url)
if (parsed.protocol === "https:" || parsed.protocol === "http:") {
return url
}
} catch {
return null
}
}
return null
}
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 15 additions & 3 deletions packages/ai-sdk/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ type AddMemoryInput = {
memory: string
}

// The schema constrains well-behaved models; a prompt-injected one can still send anything.
function clampSearchLimit(value: unknown): number {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return 10
return Math.min(50, Math.max(1, Math.floor(parsed)))
}

/**
* Create Supermemory tools for AI SDK
*/
Expand All @@ -30,6 +37,8 @@ export function supermemoryTools(
) {
const client = new Supermemory({
apiKey,
timeout: 30_000,
maxRetries: 2,
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
})

Expand All @@ -54,8 +63,10 @@ export function supermemoryTools(
default: true,
},
limit: {
type: "number",
description: "Maximum number of results to return",
type: "integer",
minimum: 1,
maximum: 50,
description: "Maximum number of results to return (1-50)",
default: 10,
},
},
Expand All @@ -67,10 +78,11 @@ export function supermemoryTools(
limit = 10,
}) => {
try {
const safeLimit = clampSearchLimit(limit)
const response = await client.search.execute({
q: informationToGet,
containerTags,
limit,
limit: safeLimit,
chunkThreshold: 0.6,
includeFullDocs,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@supermemory/tools",
"type": "module",
"version": "2.1.1",
"version": "2.2.0",
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
"scripts": {
"build": "tsdown",
Expand Down
8 changes: 7 additions & 1 deletion packages/tools/src/ai-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,4 +372,10 @@ export function supermemoryTools(
}
}

export { withSupermemory } from "./vercel"
// `./vercel` is not a published subpath, so this is the only way consumers reach the middleware types.
export {
withSupermemory,
type WithSupermemoryOptions,
type PromptTemplate,
type MemoryPromptData,
} from "./vercel"
8 changes: 4 additions & 4 deletions packages/tools/src/openai/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type OpenAI from "openai"
import { validateApiKey } from "../shared"
import {
createOpenAIMiddleware,
type OpenAIMiddlewareOptions,
Expand All @@ -21,6 +22,7 @@ import {
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
* @param options.mode - Optional mode for memory search: "profile" (default), "query", or "full"
* @param options.addMemory - Optional mode for memory addition: "always" (default), "never"
* @param options.apiKey - Optional Supermemory API key to use instead of the SUPERMEMORY_API_KEY environment variable
*
* @returns An OpenAI client with SuperMemory middleware injected for both Chat Completions and Responses APIs
*
Expand Down Expand Up @@ -56,16 +58,14 @@ import {
* })
* ```
*
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
* @throws {Error} When supermemory API request fails
*/
export function withSupermemory(
openaiClient: OpenAI,
options: OpenAIMiddlewareOptions,
) {
if (!process.env.SUPERMEMORY_API_KEY) {
throw new Error("SUPERMEMORY_API_KEY is not set")
}
validateApiKey(options.apiKey)

if (!options.containerTag) {
throw new Error(
Expand Down
30 changes: 22 additions & 8 deletions packages/tools/src/openai/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type OpenAI from "openai"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import { validateApiKey } from "../shared"
import { deduplicateMemoriesForMode } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"
Expand All @@ -20,6 +21,7 @@ export interface OpenAIMiddlewareOptions {
mode?: "profile" | "query" | "full"
addMemory?: "always" | "never"
baseUrl?: string
apiKey?: string
}

interface SupermemoryProfileSearch {
Expand Down Expand Up @@ -75,22 +77,25 @@ const getLastUserMessage = (
*
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID)
* @param queryText - Optional query text to search for specific memories. If empty, returns all profile memories
* @param baseUrl - The Supermemory API base URL
* @param apiKey - The Supermemory API key used to authenticate the request
* @returns Promise that resolves to the SuperMemory profile search response
* @throws {Error} When the API request fails or returns an error status
*
* @example
* ```typescript
* // Search with query
* const results = await supermemoryProfileSearch("user-123", "favorite programming language")
* const results = await supermemoryProfileSearch("user-123", "favorite programming language", baseUrl, apiKey)
*
* // Get all profile memories
* const profile = await supermemoryProfileSearch("user-123", "")
* const profile = await supermemoryProfileSearch("user-123", "", baseUrl, apiKey)
* ```
*/
const supermemoryProfileSearch = async (
containerTag: string,
queryText: string,
baseUrl: string,
apiKey: string,
): Promise<SupermemoryProfileSearch> => {
const payload = queryText
? JSON.stringify({
Expand All @@ -106,7 +111,7 @@ const supermemoryProfileSearch = async (
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
Authorization: `Bearer ${apiKey}`,
},
body: payload,
})
Expand Down Expand Up @@ -138,6 +143,8 @@ const supermemoryProfileSearch = async (
* @param containerTag - The container tag/identifier for memory search
* @param logger - Logger instance for debugging and info output
* @param mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both)
* @param baseUrl - The Supermemory API base URL
* @param apiKey - The Supermemory API key used to authenticate the request
* @returns Promise that resolves to enhanced messages with memory-injected system prompt
*
* @example
Expand All @@ -150,7 +157,9 @@ const supermemoryProfileSearch = async (
* messages,
* "user-123",
* logger,
* "full"
* "full",
* baseUrl,
* apiKey
* )
* // Returns messages with system prompt containing relevant memories
* ```
Expand All @@ -161,6 +170,7 @@ const addSystemPrompt = async (
logger: Logger,
mode: "profile" | "query" | "full",
baseUrl: string,
apiKey: string,
) => {
const systemPromptExists = messages.some((msg) => msg.role === "system")

Expand All @@ -170,6 +180,7 @@ const addSystemPrompt = async (
containerTag,
queryText,
baseUrl,
apiKey,
)

const memoryCountStatic = memoriesResponse.profile.static?.length || 0
Expand Down Expand Up @@ -400,8 +411,9 @@ const addMemoryTool = async (
* @param options.verbose - Enable detailed logging of memory operations (default: false)
* @param options.mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) (default: "profile")
* @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "always")
* @param options.apiKey - Supermemory API key to use instead of the SUPERMEMORY_API_KEY environment variable
* @returns Object with `wrapClient` and `createClient` methods
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
*
* @example
* ```typescript
Expand All @@ -421,8 +433,9 @@ export function createOpenAIMiddleware(
) {
const logger = createLogger(options?.verbose ?? false)
const baseUrl = normalizeBaseUrl(options?.baseUrl)
const apiKey = validateApiKey(options?.apiKey)
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY,
apiKey,
...(baseUrl !== "https://api.supermemory.ai" ? { baseURL: baseUrl } : {}),
})

Expand Down Expand Up @@ -457,6 +470,7 @@ export function createOpenAIMiddleware(
containerTag,
queryText,
baseUrl,
apiKey,
)

const memoryCountStatic = memoriesResponse.profile.static?.length || 0
Expand Down Expand Up @@ -615,15 +629,15 @@ export function createOpenAIMiddleware(
memoryCustomId,
logger,
messages,
process.env.SUPERMEMORY_API_KEY,
apiKey,
baseUrl,
),
)
}
}

operations.push(
addSystemPrompt(messages, containerTag, logger, mode, baseUrl),
addSystemPrompt(messages, containerTag, logger, mode, baseUrl, apiKey),
)

const results = await Promise.all(operations)
Expand Down
17 changes: 16 additions & 1 deletion packages/tools/src/openai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,14 @@ export function getToolDefinitions(): OpenAI.Chat.Completions.ChatCompletionTool
]
}

function parseToolArguments(argumentsJson: string) {
try {
return { success: true as const, value: JSON.parse(argumentsJson) }
} catch {
return { success: false as const }
}
}

/**
* Execute a tool call based on the function name and arguments
*/
Expand All @@ -565,7 +573,14 @@ export function createToolCallExecutor(
toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall,
): Promise<string> {
const functionName = toolCall.function.name
const args = JSON.parse(toolCall.function.arguments)
const parsed = parseToolArguments(toolCall.function.arguments)
if (!parsed.success) {
return JSON.stringify({
success: false,
error: `Invalid JSON arguments for ${functionName}`,
})
}
const args = parsed.value

switch (functionName) {
case "searchMemories":
Expand Down
6 changes: 1 addition & 5 deletions packages/tools/src/voltagent/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ export function createSupermemoryHooks(
return
}

saveConversation(messages, ctx).catch((error) => {
ctx.logger.error("Background conversation save failed", {
error: error instanceof Error ? error.message : "Unknown error",
})
})
await saveConversation(messages, ctx)
} catch (error) {
ctx.logger.error("Error in onEnd", {
error: error instanceof Error ? error.message : "Unknown error",
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/src/voltagent/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ const convertToConversationMessages = (
}

/**
* Saves conversation to Supermemory (fire-and-forget).
* Saves conversation to Supermemory.
*/
export const saveConversation = async (
messages: VoltAgentMessage[],
Expand Down
Loading
Loading