Skip to main content
Glama

Karakeep MCP server

by karakeep-app
assets.ts5.96 kB
import { TRPCError } from "@trpc/server"; import { and, desc, eq, sql } from "drizzle-orm"; import { z } from "zod"; import { assets, bookmarks } from "@karakeep/db/schema"; import { deleteAsset } from "@karakeep/shared/assetdb"; import { zAssetSchema, zAssetTypesSchema, } from "@karakeep/shared/types/bookmarks"; import { authedProcedure, Context, router } from "../index"; import { isAllowedToAttachAsset, isAllowedToDetachAsset, mapDBAssetTypeToUserType, mapSchemaAssetTypeToDB, } from "../lib/attachments"; import { ensureBookmarkOwnership } from "./bookmarks"; export const ensureAssetOwnership = async (opts: { ctx: Context; assetId: string; }) => { const asset = await opts.ctx.db.query.assets.findFirst({ where: eq(bookmarks.id, opts.assetId), }); if (!opts.ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized", }); } if (!asset) { throw new TRPCError({ code: "NOT_FOUND", message: "Asset not found", }); } if (asset.userId != opts.ctx.user.id) { throw new TRPCError({ code: "FORBIDDEN", message: "User is not allowed to access resource", }); } return asset; }; export const assetsAppRouter = router({ list: authedProcedure .input( z.object({ limit: z.number().min(1).max(100).default(20), cursor: z.number().nullish(), // page number }), ) .output( z.object({ assets: z.array( z.object({ id: z.string(), assetType: zAssetTypesSchema, size: z.number(), contentType: z.string().nullable(), fileName: z.string().nullable(), bookmarkId: z.string().nullable(), }), ), nextCursor: z.number().nullish(), totalCount: z.number(), }), ) .query(async ({ input, ctx }) => { const page = input.cursor ?? 1; const [results, totalCount] = await Promise.all([ ctx.db .select() .from(assets) .where(eq(assets.userId, ctx.user.id)) .orderBy(desc(assets.size)) .limit(input.limit) .offset((page - 1) * input.limit), ctx.db .select({ count: sql<number>`count(*)` }) .from(assets) .where(eq(assets.userId, ctx.user.id)), ]); return { assets: results.map((a) => ({ ...a, assetType: mapDBAssetTypeToUserType(a.assetType), })), nextCursor: page * input.limit < totalCount[0].count ? page + 1 : null, totalCount: totalCount[0].count, }; }), attachAsset: authedProcedure .input( z.object({ bookmarkId: z.string(), asset: zAssetSchema, }), ) .output(zAssetSchema) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { await ensureAssetOwnership({ ctx, assetId: input.asset.id }); if (!isAllowedToAttachAsset(input.asset.assetType)) { throw new TRPCError({ code: "BAD_REQUEST", message: "You can't attach this type of asset", }); } await ctx.db .update(assets) .set({ assetType: mapSchemaAssetTypeToDB(input.asset.assetType), bookmarkId: input.bookmarkId, }) .where( and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)), ); return input.asset; }), replaceAsset: authedProcedure .input( z.object({ bookmarkId: z.string(), oldAssetId: z.string(), newAssetId: z.string(), }), ) .output(z.void()) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { await Promise.all([ ensureAssetOwnership({ ctx, assetId: input.oldAssetId }), ensureAssetOwnership({ ctx, assetId: input.newAssetId }), ]); const [oldAsset] = await ctx.db .select() .from(assets) .where( and(eq(assets.id, input.oldAssetId), eq(assets.userId, ctx.user.id)), ) .limit(1); if ( !isAllowedToAttachAsset(mapDBAssetTypeToUserType(oldAsset.assetType)) ) { throw new TRPCError({ code: "BAD_REQUEST", message: "You can't attach this type of asset", }); } await ctx.db.transaction(async (tx) => { await tx.delete(assets).where(eq(assets.id, input.oldAssetId)); await tx .update(assets) .set({ bookmarkId: input.bookmarkId, assetType: oldAsset.assetType, }) .where(eq(assets.id, input.newAssetId)); }); await deleteAsset({ userId: ctx.user.id, assetId: input.oldAssetId, }).catch(() => ({})); }), detachAsset: authedProcedure .input( z.object({ bookmarkId: z.string(), assetId: z.string(), }), ) .output(z.void()) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { await ensureAssetOwnership({ ctx, assetId: input.assetId }); const [oldAsset] = await ctx.db .select() .from(assets) .where( and(eq(assets.id, input.assetId), eq(assets.userId, ctx.user.id)), ); if ( !isAllowedToDetachAsset(mapDBAssetTypeToUserType(oldAsset.assetType)) ) { throw new TRPCError({ code: "BAD_REQUEST", message: "You can't deattach this type of asset", }); } const result = await ctx.db .delete(assets) .where( and( eq(assets.id, input.assetId), eq(assets.bookmarkId, input.bookmarkId), ), ); if (result.changes == 0) { throw new TRPCError({ code: "NOT_FOUND" }); } await deleteAsset({ userId: ctx.user.id, assetId: input.assetId }).catch( () => ({}), ); }), });

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