Skip to main content
Glama

Karakeep MCP server

by karakeep-app
bookmarks.test.ts20.3 kB
import { assert, beforeEach, describe, expect, inject, it } from "vitest"; import { createKarakeepClient } from "@karakeep/sdk"; import { createTestUser } from "../../utils/api"; describe("Bookmarks API", () => { const port = inject("karakeepPort"); if (!port) { throw new Error("Missing required environment variables"); } let client: ReturnType<typeof createKarakeepClient>; let apiKey: string; beforeEach(async () => { apiKey = await createTestUser(); client = createKarakeepClient({ baseUrl: `http://localhost:${port}/api/v1/`, headers: { "Content-Type": "application/json", authorization: `Bearer ${apiKey}`, }, }); }); it("should create and retrieve a bookmark", async () => { // Create a new bookmark const { data: createdBookmark, response: createResponse, error, } = await client.POST("/bookmarks", { body: { type: "text", title: "Test Bookmark", text: "This is a test bookmark", }, }); if (error) { console.error("Error creating bookmark:", error); throw error; } expect(createResponse.status).toBe(201); expect(createdBookmark).toBeDefined(); expect(createdBookmark?.id).toBeDefined(); // Get the created bookmark const { data: retrievedBookmark, response: getResponse } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId: createdBookmark.id, }, }, }, ); expect(getResponse.status).toBe(200); expect(retrievedBookmark!.id).toBe(createdBookmark.id); expect(retrievedBookmark!.title).toBe("Test Bookmark"); assert(retrievedBookmark!.content.type === "text"); expect(retrievedBookmark!.content.text).toBe("This is a test bookmark"); }); it("should update a bookmark", async () => { // Create a new bookmark const { data: createdBookmark, error: createError } = await client.POST( "/bookmarks", { body: { type: "text", title: "Test Bookmark", text: "This is a test bookmark", }, }, ); if (createError) { console.error("Error creating bookmark:", createError); throw createError; } if (!createdBookmark) { throw new Error("Bookmark creation failed"); } // Update the bookmark const { data: updatedBookmark, response: updateResponse } = await client.PATCH("/bookmarks/{bookmarkId}", { params: { path: { bookmarkId: createdBookmark.id, }, }, body: { title: "Updated Title", }, }); expect(updateResponse.status).toBe(200); expect(updatedBookmark!.title).toBe("Updated Title"); }); it("should delete a bookmark", async () => { // Create a new bookmark const { data: createdBookmark, error: createError } = await client.POST( "/bookmarks", { body: { type: "text", title: "Test Bookmark", text: "This is a test bookmark", }, }, ); if (createError) { console.error("Error creating bookmark:", createError); throw createError; } if (!createdBookmark) { throw new Error("Bookmark creation failed"); } // Delete the bookmark const { response: deleteResponse } = await client.DELETE( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId: createdBookmark.id, }, }, }, ); expect(deleteResponse.status).toBe(204); // Verify it's deleted const { response: getResponse } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId: createdBookmark.id, }, }, }, ); expect(getResponse.status).toBe(404); }); it("should paginate through bookmarks", async () => { // Create multiple bookmarks const bookmarkPromises = Array.from({ length: 5 }, (_, i) => client.POST("/bookmarks", { body: { type: "text", title: `Test Bookmark ${i}`, text: `This is test bookmark ${i}`, }, }), ); const createdBookmarks = await Promise.all(bookmarkPromises); const bookmarkIds = createdBookmarks.map((b) => b.data!.id); // Get first page const { data: firstPage, response: firstResponse } = await client.GET( "/bookmarks", { params: { query: { limit: 2, }, }, }, ); expect(firstResponse.status).toBe(200); expect(firstPage!.bookmarks.length).toBe(2); expect(firstPage!.nextCursor).toBeDefined(); // Get second page const { data: secondPage, response: secondResponse } = await client.GET( "/bookmarks", { params: { query: { limit: 2, cursor: firstPage!.nextCursor!, }, }, }, ); expect(secondResponse.status).toBe(200); expect(secondPage!.bookmarks.length).toBe(2); expect(secondPage!.nextCursor).toBeDefined(); // Get final page const { data: finalPage, response: finalResponse } = await client.GET( "/bookmarks", { params: { query: { limit: 2, cursor: secondPage!.nextCursor!, }, }, }, ); expect(finalResponse.status).toBe(200); expect(finalPage!.bookmarks.length).toBe(1); expect(finalPage!.nextCursor).toBeNull(); // Verify all bookmarks were returned const allBookmarks = [ ...firstPage!.bookmarks, ...secondPage!.bookmarks, ...finalPage!.bookmarks, ]; expect(allBookmarks.map((b) => b.id)).toEqual( expect.arrayContaining(bookmarkIds), ); }); it("should manage tags on a bookmark", async () => { // Create a new bookmark const { data: createdBookmark, error: createError } = await client.POST( "/bookmarks", { body: { type: "text", title: "Test Bookmark", text: "This is a test bookmark", }, }, ); if (createError) { console.error("Error creating bookmark:", createError); throw createError; } if (!createdBookmark) { throw new Error("Bookmark creation failed"); } // Add tags const { data: addTagsResponse, response: addTagsRes } = await client.POST( "/bookmarks/{bookmarkId}/tags", { params: { path: { bookmarkId: createdBookmark.id, }, }, body: { tags: [{ tagName: "test-tag" }], }, }, ); expect(addTagsRes.status).toBe(200); expect(addTagsResponse!.attached.length).toBe(1); // Remove tags const { response: removeTagsRes } = await client.DELETE( "/bookmarks/{bookmarkId}/tags", { params: { path: { bookmarkId: createdBookmark.id, }, }, body: { tags: [{ tagId: addTagsResponse!.attached[0] }], }, }, ); expect(removeTagsRes.status).toBe(200); }); it("should get lists for a bookmark", async () => { const { data: createdBookmark } = await client.POST("/bookmarks", { body: { type: "text", title: "Test Bookmark", text: "This is a test bookmark", }, }); const { data: createdList } = await client.POST("/lists", { body: { name: "Test List", icon: "📚", }, }); const { response: addBookmarkResponse } = await client.PUT( "/lists/{listId}/bookmarks/{bookmarkId}", { params: { path: { listId: createdList!.id, bookmarkId: createdBookmark!.id, }, }, }, ); expect(addBookmarkResponse.status).toBe(204); const { data: lists, response: getListsResponse } = await client.GET( "/bookmarks/{bookmarkId}/lists", { params: { path: { bookmarkId: createdBookmark!.id, }, }, }, ); expect(getListsResponse.status).toBe(200); expect(lists!.lists.length).toBe(1); expect(lists!.lists[0].id).toBe(createdList!.id); expect(lists!.lists[0].name).toBe("Test List"); expect(lists!.lists[0].icon).toBe("📚"); }); it("should search bookmarks", async () => { // Create test bookmarks await client.POST("/bookmarks", { body: { type: "text", title: "Search Test 1", text: "This is a test bookmark for search", }, }); await client.POST("/bookmarks", { body: { type: "text", title: "Search Test 2", text: "Another test bookmark for search", }, }); // Wait 3 seconds for the search index to be updated // TODO: Replace with a check that all queues are empty await new Promise((f) => setTimeout(f, 3000)); // Search for bookmarks const { data: searchResults, response: searchResponse } = await client.GET( "/bookmarks/search", { params: { query: { q: "test bookmark", }, }, }, ); expect(searchResponse.status).toBe(200); expect(searchResults!.bookmarks.length).toBeGreaterThanOrEqual(2); }); it("should paginate search results", async () => { // Create multiple bookmarks const bookmarkPromises = Array.from({ length: 5 }, (_, i) => client.POST("/bookmarks", { body: { type: "text", title: `Search Pagination ${i}`, text: `This is test bookmark ${i} for pagination`, }, }), ); await Promise.all(bookmarkPromises); // Wait 3 seconds for the search index to be updated // TODO: Replace with a check that all queues are empty await new Promise((f) => setTimeout(f, 3000)); // Get first page const { data: firstPage, response: firstResponse } = await client.GET( "/bookmarks/search", { params: { query: { q: "pagination", limit: 2, }, }, }, ); expect(firstResponse.status).toBe(200); expect(firstPage!.bookmarks.length).toBe(2); expect(firstPage!.nextCursor).toBeDefined(); // Get second page const { data: secondPage, response: secondResponse } = await client.GET( "/bookmarks/search", { params: { query: { q: "pagination", limit: 2, cursor: firstPage!.nextCursor!, }, }, }, ); expect(secondResponse.status).toBe(200); expect(secondPage!.bookmarks.length).toBe(2); expect(secondPage!.nextCursor).toBeDefined(); // Get final page const { data: finalPage, response: finalResponse } = await client.GET( "/bookmarks/search", { params: { query: { q: "pagination", limit: 2, cursor: secondPage!.nextCursor!, }, }, }, ); expect(finalResponse.status).toBe(200); expect(finalPage!.bookmarks.length).toBe(1); expect(finalPage!.nextCursor).toBeNull(); }); describe("singlefile", () => { async function uploadSinglefileAsset(ifexists?: string) { const file = new File(["<html>HELLO WORLD</html>"], "test.html", { type: "text/html", }); const formData = new FormData(); formData.append("url", "https://example.com"); formData.append("file", file); const url = new URL( `http://localhost:${port}/api/v1/bookmarks/singlefile`, ); if (ifexists) { url.searchParams.append("ifexists", ifexists); } const response = await fetch(url.toString(), { method: "POST", headers: { authorization: `Bearer ${apiKey}`, }, body: formData, }); if (!response.ok) { return [null, response] as const; } const data = (await response.json()) as { id: string }; return [data, response] as const; } it("should support precrawling via singlefile with ifexists=skip", async () => { // First upload: create a bookmark const [data, response] = await uploadSinglefileAsset(); expect(response?.status).toBe(201); const bookmarkId = data?.id; if (!bookmarkId) throw new Error("Bookmark ID not found"); // Get the bookmark and record the precrawled asset id const { data: bookmark, response: getResponse1 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse1.status).toBe(200); const assetIds = bookmark!.assets .filter((a) => a.assetType === "precrawledArchive") .map((a) => a.id); expect(assetIds.length).toBe(1); const firstAssetId = assetIds[0]; // Second upload with skip const [data2, response2] = await uploadSinglefileAsset("skip"); expect(response2?.status).toBe(200); expect(data2?.id).toBe(bookmarkId); // Get the bookmark again const { data: bookmark2, response: getResponse2 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse2.status).toBe(200); const assetIds2 = bookmark2!.assets .filter((a) => a.assetType === "precrawledArchive") .map((a) => a.id); expect(assetIds2).toEqual([firstAssetId]); // same asset }); it("should support precrawling via singlefile with ifexists=overwrite", async () => { // First upload const [data, response] = await uploadSinglefileAsset("overwrite"); expect(response?.status).toBe(201); const bookmarkId = data?.id; if (!bookmarkId) throw new Error("Bookmark ID not found"); // Record the asset const { data: bookmark, response: getResponse1 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse1.status).toBe(200); const firstAssetId = bookmark!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(firstAssetId).toBeDefined(); // Second upload with overwrite const [data2, response2] = await uploadSinglefileAsset("overwrite"); expect(response2?.status).toBe(200); expect(data2?.id).toBe(bookmarkId); // Get the bookmark again const { data: bookmark2, response: getResponse2 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse2.status).toBe(200); const secondAssetId = bookmark2!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(secondAssetId).toBeDefined(); expect(secondAssetId).not.toBe(firstAssetId); // There should be only one precrawledArchive asset const precrawledAssets = bookmark2!.assets.filter( (a) => a.assetType === "precrawledArchive", ); expect(precrawledAssets.length).toBe(1); }); it("should support precrawling via singlefile with ifexists=overwrite-recrawl", async () => { // First upload const [data, response] = await uploadSinglefileAsset("overwrite-recrawl"); expect(response?.status).toBe(201); const bookmarkId = data?.id; if (!bookmarkId) throw new Error("Bookmark ID not found"); // Record the asset const { data: bookmark, response: getResponse1 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse1.status).toBe(200); const firstAssetId = bookmark!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(firstAssetId).toBeDefined(); // Second upload with overwrite-recrawl const [data2, response2] = await uploadSinglefileAsset("overwrite-recrawl"); expect(response2?.status).toBe(200); expect(data2?.id).toBe(bookmarkId); // Get the bookmark again const { data: bookmark2, response: getResponse2 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse2.status).toBe(200); const secondAssetId = bookmark2!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(secondAssetId).toBeDefined(); expect(secondAssetId).not.toBe(firstAssetId); // There should be only one precrawledArchive asset const precrawledAssets = bookmark2!.assets.filter( (a) => a.assetType === "precrawledArchive", ); expect(precrawledAssets.length).toBe(1); }); it("should support precrawling via singlefile with ifexists=append", async () => { // First upload const [data, response] = await uploadSinglefileAsset("append"); expect(response?.status).toBe(201); const bookmarkId = data?.id; if (!bookmarkId) throw new Error("Bookmark ID not found"); // Record the first asset const { data: bookmark, response: getResponse1 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse1.status).toBe(200); const firstAssetId = bookmark!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(firstAssetId).toBeDefined(); // Second upload with append const [data2, response2] = await uploadSinglefileAsset("append"); expect(response2?.status).toBe(200); expect(data2?.id).toBe(bookmarkId); // Get the bookmark again const { data: bookmark2, response: getResponse2 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse2.status).toBe(200); const precrawledAssets = bookmark2!.assets.filter( (a) => a.assetType === "precrawledArchive", ); expect(precrawledAssets.length).toBe(2); expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId); // The second asset id should be different const secondAssetId = precrawledAssets.find( (asset) => asset.id !== firstAssetId, )?.id; expect(secondAssetId).toBeDefined(); }); it("should support precrawling via singlefile with ifexists=append-recrawl", async () => { // First upload const [data, response] = await uploadSinglefileAsset("append-recrawl"); expect(response?.status).toBe(201); const bookmarkId = data?.id; if (!bookmarkId) throw new Error("Bookmark ID not found"); // Record the first asset const { data: bookmark, response: getResponse1 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse1.status).toBe(200); const firstAssetId = bookmark!.assets.find( (a) => a.assetType === "precrawledArchive", )?.id; expect(firstAssetId).toBeDefined(); // Second upload with append-recrawl const [data2, response2] = await uploadSinglefileAsset("append-recrawl"); expect(response2?.status).toBe(200); expect(data2?.id).toBe(bookmarkId); // Get the bookmark again const { data: bookmark2, response: getResponse2 } = await client.GET( "/bookmarks/{bookmarkId}", { params: { path: { bookmarkId } }, }, ); expect(getResponse2.status).toBe(200); const precrawledAssets = bookmark2!.assets.filter( (a) => a.assetType === "precrawledArchive", ); expect(precrawledAssets.length).toBe(2); expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId); // The second asset id should be different const secondAssetId = precrawledAssets.find( (asset) => asset.id !== firstAssetId, )?.id; expect(secondAssetId).toBeDefined(); }); }); });

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/karakeep-app/karakeep'

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