Skip to main content
Glama

Karakeep MCP server

by karakeep-app
ruleEngine.ts6.05 kB
import deepEql from "deep-equal"; import { and, eq } from "drizzle-orm"; import { bookmarks, tagsOnBookmarks } from "@karakeep/db/schema"; import { LinkCrawlerQueue } from "@karakeep/shared-server"; import { RuleEngineAction, RuleEngineCondition, RuleEngineEvent, RuleEngineRule, } from "@karakeep/shared/types/rules"; import { AuthedContext } from ".."; import { List } from "../models/lists"; import { RuleEngineRuleModel } from "../models/rules"; async function fetchBookmark(db: AuthedContext["db"], bookmarkId: string) { return await db.query.bookmarks.findFirst({ where: eq(bookmarks.id, bookmarkId), with: { link: { columns: { url: true, }, }, text: true, asset: true, tagsOnBookmarks: true, rssFeeds: { columns: { rssFeedId: true, }, }, user: { columns: {}, with: { rules: { with: { actions: true, }, }, }, }, }, }); } type ReturnedBookmark = NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>; export interface RuleEngineEvaluationResult { type: "success" | "failure"; ruleId: string; message: string; } export class RuleEngine { private constructor( private ctx: AuthedContext, private bookmark: Omit<ReturnedBookmark, "user">, private rules: RuleEngineRule[], ) {} static async forBookmark(ctx: AuthedContext, bookmarkId: string) { const [bookmark, rules] = await Promise.all([ fetchBookmark(ctx.db, bookmarkId), RuleEngineRuleModel.getAll(ctx), ]); if (!bookmark) { throw new Error(`Bookmark ${bookmarkId} not found`); } return new RuleEngine( ctx, bookmark, rules.map((r) => r.rule), ); } doesBookmarkMatchConditions(condition: RuleEngineCondition): boolean { switch (condition.type) { case "alwaysTrue": { return true; } case "urlContains": { return (this.bookmark.link?.url ?? "").includes(condition.str); } case "importedFromFeed": { return this.bookmark.rssFeeds.some( (f) => f.rssFeedId === condition.feedId, ); } case "bookmarkTypeIs": { return this.bookmark.type === condition.bookmarkType; } case "hasTag": { return this.bookmark.tagsOnBookmarks.some( (t) => t.tagId === condition.tagId, ); } case "isFavourited": { return this.bookmark.favourited; } case "isArchived": { return this.bookmark.archived; } case "and": { return condition.conditions.every((c) => this.doesBookmarkMatchConditions(c), ); } case "or": { return condition.conditions.some((c) => this.doesBookmarkMatchConditions(c), ); } default: { const _exhaustiveCheck: never = condition; return false; } } } async evaluateRule( rule: RuleEngineRule, event: RuleEngineEvent, ): Promise<RuleEngineEvaluationResult[]> { if (!rule.enabled) { return []; } if (!deepEql(rule.event, event, { strict: true })) { return []; } if (!this.doesBookmarkMatchConditions(rule.condition)) { return []; } const results = await Promise.allSettled( rule.actions.map((action) => this.executeAction(action)), ); return results.map((result) => { if (result.status === "fulfilled") { return { type: "success", ruleId: rule.id, message: result.value, }; } else { return { type: "failure", ruleId: rule.id, message: (result.reason as Error).message, }; } }); } async executeAction(action: RuleEngineAction): Promise<string> { switch (action.type) { case "addTag": { await this.ctx.db .insert(tagsOnBookmarks) .values([ { attachedBy: "human", bookmarkId: this.bookmark.id, tagId: action.tagId, }, ]) .onConflictDoNothing(); return `Added tag ${action.tagId}`; } case "removeTag": { await this.ctx.db .delete(tagsOnBookmarks) .where( and( eq(tagsOnBookmarks.tagId, action.tagId), eq(tagsOnBookmarks.bookmarkId, this.bookmark.id), ), ); return `Removed tag ${action.tagId}`; } case "addToList": { const list = await List.fromId(this.ctx, action.listId); await list.addBookmark(this.bookmark.id); return `Added to list ${action.listId}`; } case "removeFromList": { const list = await List.fromId(this.ctx, action.listId); await list.removeBookmark(this.bookmark.id); return `Removed from list ${action.listId}`; } case "downloadFullPageArchive": { await LinkCrawlerQueue.enqueue({ bookmarkId: this.bookmark.id, archiveFullPage: true, runInference: false, }); return `Enqueued full page archive`; } case "favouriteBookmark": { await this.ctx.db .update(bookmarks) .set({ favourited: true, }) .where(eq(bookmarks.id, this.bookmark.id)); return `Marked as favourited`; } case "archiveBookmark": { await this.ctx.db .update(bookmarks) .set({ archived: true, }) .where(eq(bookmarks.id, this.bookmark.id)); return `Marked as archived`; } default: { const _exhaustiveCheck: never = action; return ""; } } } async onEvent(event: RuleEngineEvent): Promise<RuleEngineEvaluationResult[]> { const results = await Promise.all( this.rules.map((rule) => this.evaluateRule(rule, event)), ); return results.flat(); } }

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