@getrember/mcp
Version:
A command line tool for setting up Rember MCP server
439 lines (431 loc) • 19.2 kB
JavaScript
;
var cli = require('@effect/cli');
var platformNode = require('@effect/platform-node');
var NodeContext = require('@effect/platform-node/NodeContext');
var NodeRuntime = require('@effect/platform-node/NodeRuntime');
var effect = require('effect');
var Effect4 = require('effect/Effect');
var platform = require('@effect/platform');
var MCP = require('@modelcontextprotocol/sdk/server/index.js');
var MCPstdio = require('@modelcontextprotocol/sdk/server/stdio.js');
var types_js = require('@modelcontextprotocol/sdk/types.js');
var JsonSchema = require('effect/JSONSchema');
var AST = require('effect/SchemaAST');
var ai = require('@effect/ai');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var NodeContext__namespace = /*#__PURE__*/_interopNamespace(NodeContext);
var NodeRuntime__namespace = /*#__PURE__*/_interopNamespace(NodeRuntime);
var Effect4__namespace = /*#__PURE__*/_interopNamespace(Effect4);
var MCP__namespace = /*#__PURE__*/_interopNamespace(MCP);
var MCPstdio__namespace = /*#__PURE__*/_interopNamespace(MCPstdio);
var JsonSchema__namespace = /*#__PURE__*/_interopNamespace(JsonSchema);
var AST__namespace = /*#__PURE__*/_interopNamespace(AST);
var layerLogger = effect.Logger.replace(
effect.Logger.defaultLogger,
effect.Logger.prettyLogger({
// We are currently using stdio as MCP transport, therefore we cannot log
// on stdout, we have to log in stderr.
stderr: true,
colors: true,
mode: "tty"
})
);
var ApiKey = effect.Schema.String.pipe(
effect.Schema.pattern(/^rember_[a-f0-9]{32}$/),
effect.Schema.brand("ApiKey")
);
var Note = effect.Schema.Struct({
text: effect.Schema.String.pipe(effect.Schema.maxLength(2e3))
});
var Notes = effect.Schema.Array(Note).pipe(effect.Schema.maxItems(50));
var ErrorApiKeyInvalid = class extends effect.Schema.TaggedError()(
"Api/ApiKeyInvalid",
{ message: effect.Schema.String }
) {
};
var ErrorReachedLimitRateLimiter = class extends effect.Schema.TaggedError()(
"Api/ReachedLimitRateLimiter",
{ message: effect.Schema.String }
) {
};
var ErrorReachedLimitUsageTracker = class extends effect.Schema.TaggedError()(
"Api/ReachedLimitUsageTracker",
{ message: effect.Schema.String }
) {
};
var ErrorReachedLimitQuantity = class extends effect.Schema.TaggedError()(
"Api/ErrorReachedLimitQuantity",
{ message: effect.Schema.String }
) {
};
var endpointGenerateCardsAndCreateRembs = platform.HttpApiEndpoint.post(
"generateCardsAndCreateRembs",
"/v1/generate-cards-and-create-rembs"
).setPayload(
effect.Schema.Struct({
version: effect.Schema.Literal("1"),
notes: Notes
})
).setHeaders(
effect.Schema.Struct({
"x-api-key": ApiKey,
"x-source": effect.Schema.String
})
).addSuccess(
effect.Schema.Union(
effect.Schema.Struct({
quantity: effect.Schema.Number,
usageMonth: effect.Schema.Number,
maxUsageMonth: effect.Schema.Number
})
)
).addError(ErrorApiKeyInvalid, { status: 401 }).addError(ErrorReachedLimitQuantity, { status: 400 }).addError(ErrorReachedLimitUsageTracker, { status: 403 }).addError(ErrorReachedLimitRateLimiter, { status: 429 });
var groupV1 = platform.HttpApiGroup.make("v1").add(endpointGenerateCardsAndCreateRembs);
var apiRember = platform.HttpApi.make("Rember").add(groupV1).prefix("/api");
var Rember = class extends effect.Context.Tag("Rember")() {
};
var makeRember = effect.Effect.gen(function* () {
const client = yield* platform.HttpApiClient.make(apiRember, {
baseUrl: "https://www.rember.com/",
transformClient: platform.HttpClient.retryTransient({
times: 3,
schedule: effect.Schedule.exponential("2 seconds")
})
});
const apiKeyEnc = yield* effect.Config.string("REMBER_API_KEY");
const apiKey2 = yield* effect.pipe(
apiKeyEnc,
effect.Schema.decodeUnknown(ApiKey),
effect.Effect.catchTag("ParseError", () => effect.Effect.dieMessage("Invalid API key"))
);
const generateCardsAndCreateRembs = ({ notes }) => client.v1.generateCardsAndCreateRembs({
payload: { version: "1", notes },
headers: { "x-api-key": apiKey2, "x-source": "rember-mcp" }
});
return {
generateCardsAndCreateRembs
};
});
var layerRember = effect.Layer.effect(Rember, makeRember);
var ErrorServerMCP = class extends effect.Data.TaggedError("ErrorServerMCP") {
};
var ErrorToolMCP = class extends effect.Schema.TaggedError()("ErrorToolMCP", {
message: effect.Schema.String
}) {
};
var ServerMCP = class extends effect.Context.Tag("ServerMCP")() {
};
var makeServerMCP = ({
name,
tools,
version
}) => effect.Effect.gen(function* () {
const runtime = yield* effect.Effect.runtime();
const server = yield* effect.Effect.try({
try: () => new MCP__namespace.Server({ name, version }),
catch: (error) => new ErrorServerMCP({ message: "Failed to construct Server", error })
});
const transport = yield* effect.Effect.try({
try: () => new MCPstdio__namespace.StdioServerTransport(),
catch: (error) => new ErrorServerMCP({ message: "Failed to construct StdioServerTransport", error })
});
for (const [, tool] of tools.toolkit.tools) {
if (tool.success !== effect.Schema.String) {
return yield* effect.Effect.dieMessage("All tool `success` schemas must be `Schema.String`");
}
}
yield* effect.Effect.try({
try: () => server.registerCapabilities({ tools: {} }),
catch: (error) => new ErrorServerMCP({ message: "Failed to register tools capabilities", error })
});
const arrayTools = [];
for (const [, tool] of tools.toolkit.tools) {
arrayTools.push(convertTool(tool._tag, tool));
}
yield* effect.Effect.try({
try: () => server.setRequestHandler(
types_js.ListToolsRequestSchema,
// NOTE: Casting to `any` because of incompatibility in how JSON Schema 7
// is represented at the type level between Effect and the MCP SDK.
() => ({ tools: arrayTools })
),
catch: (error) => new ErrorServerMCP({ message: "Failed to register tools/list handler", error })
});
yield* effect.Effect.try({
try: () => server.setRequestHandler(
types_js.CallToolRequestSchema,
(request, { signal }) => effect.pipe(
effect.Effect.gen(function* () {
const tagTool = effect.String.snakeToPascal(request.params.name);
const optionTool = effect.HashMap.get(tools.toolkit.tools, tagTool);
if (effect.Option.isNone(optionTool)) {
return yield* new ErrorServerMCP({ message: `Tool '${tagTool}' not found` });
}
const tool = optionTool.value;
const handler = effect.HashMap.unsafeGet(tools.handlers, tagTool);
const params = yield* effect.pipe(
effect.Schema.decodeUnknown(tool)({
...request.params.arguments,
_tag: tagTool
}),
effect.Effect.mapError(
(error) => new ErrorServerMCP({
message: `Failed to decode tool call '${tagTool}' parameters`,
error
})
)
);
const result = yield* handler(params);
const resultText = yield* effect.pipe(
effect.Schema.encodeUnknown(tool.success)(result),
effect.Effect.mapError(
(error) => new ErrorServerMCP({
message: `Failed to encode tool call '${tagTool}' result`,
error
})
)
);
if (typeof resultText !== "string") {
return yield* effect.Effect.dieMessage("Tool call result must be string");
}
return { content: [{ type: "text", text: resultText }] };
}),
// Report errors
// Note that for `ErrorServerMCP` and `ErrorToolMCP` we report the
// error message to the MCP client. For all other errors we report
// the entire cause.
effect.Effect.catchAllCause(
(cause) => effect.Effect.gen(function* () {
if (effect.Cause.isInterruptedOnly(cause)) {
return { content: [{ type: "text", text: "The tool call was interruped" }], isError: true };
}
if (effect.Cause.isFailType(cause) && cause.error instanceof ErrorServerMCP) {
yield* effect.Effect.logError(cause.error);
return { content: [{ type: "text", text: cause.error.message }], isError: true };
}
if (effect.Cause.isFailType(cause) && cause.error instanceof ErrorToolMCP) {
yield* effect.Effect.logError(cause.error);
return { content: [{ type: "text", text: cause.error.message }], isError: true };
}
yield* effect.Effect.logError(cause);
return { content: [{ type: "text", text: effect.Cause.pretty(cause) }], isError: true };
})
),
// See TODO on `makeServerMCP` for why we cast `effect`.
(effect$1) => effect.Runtime.runPromise(runtime)(effect$1, { signal })
)
),
catch: (error) => new ErrorServerMCP({ message: "Failed to register tool call handler", error })
});
yield* effect.Effect.acquireRelease(
// `server.connect` starts the transport and starts listening for messages
effect.Effect.promise(() => server.connect(transport)),
// `server.close` closes the transport
() => effect.Effect.promise(() => server.close())
);
return {
server
};
});
function layerServerMCP({ name, tools, version }) {
return effect.Layer.scoped(ServerMCP, makeServerMCP({ name, version, tools }));
}
var convertTool = (name, schema) => ({
name: effect.String.pascalToSnake(name),
description: getDescription(schema.ast),
inputSchema: makeJsonSchema(AST__namespace.omit(schema.ast, ["_tag"]))
});
var makeJsonSchema = (ast) => {
const $defs = {};
const schema = JsonSchema__namespace.fromAST(ast, {
definitions: $defs,
topLevelReferenceStrategy: "skip"
});
if (Object.keys($defs).length === 0) return schema;
schema.$defs = $defs;
return schema;
};
var getDescription = (ast) => {
const annotations = ast._tag === "Transformation" ? {
...ast.to.annotations,
...ast.annotations
} : ast.annotations;
return AST__namespace.DescriptionAnnotationId in annotations ? annotations[AST__namespace.DescriptionAnnotationId] : "";
};
var ToolCreateFlashcards = class extends effect.Schema.TaggedRequest()(
"CreateFlashcards",
{
success: effect.Schema.String,
failure: ErrorToolMCP,
payload: {
notes: effect.Schema.Array(
effect.Schema.Struct({
text: effect.Schema.String.pipe(effect.Schema.maxLength(2e3)).annotations({
title: "Text",
description: "The text content of the note"
})
}).annotations({
title: "Note",
description: "A little note about a concept or idea"
})
).pipe(effect.Schema.maxItems(50)).annotations({
title: "Notes",
description: "A list of little notes"
}),
source: effect.Schema.String.pipe(effect.Schema.maxLength(100), effect.Schema.optional).annotations({
title: "Source",
description: "The resource (e.g. article, book, pdf, webpage) the notes are about (e.g. 'Author - Title'). Omit this field unless the notes are about a specific concrete resource."
})
}
},
{
description: effect.pipe(
`
|A tool to generate spaced-repetition flashcards in Rember.
|
|What is Rember?
|Rember is a modern spaced-repetition system based on *rembs*.
|A remb is a concise note focused on a single concept or idea you want to remember, along with a few flashcards testing that concept or idea.
|In Rember you can create rembs and review their flashcards, just like in Anki or other traditional spaced-repetition systems.
|Rember also allows exporting rembs to Anki.
|Rember can be found at https://rember.com.
|
|What is MCP?
|MCP (Model Context Protocol) is Anthropic's open standard allowing Claude to connect with external tools and data sources through a standardized interface.
|This tools is implemented and being called through MCP.
|
|Input and behavior:
|The input is a list of notes, with optionally a source. Rember will turn each note into a remb, by generating flashcards using AI.
|In particular, the notes are sent to the Rember API. The Rember API will generate the flashcards with our own custom AI prompts, independently from this conversation with you.
|Rember will often generate 4-5 flashcards for each single note.
|Rembs are the natural organizational unit for spaced-repetition flashcards, they allow users to quickly search, organize and interact with flashcards.
|Note that if the user asks about Rember in general, keep things simple and avoid getting into lower level details by e.g. mentioning the Rember API.
|
|How to use this tool:
|- The user might ask you to create a few flashcards about a topic: create one note with only the essential concepts of the topic
|- After asking you a question the user might say something like "help me remember this": create one note synthesizing only the key points of the answer
|- After chatting with you the user might ask for a few flashcards: create one or two notes capturing only the core insights from the conversation
|- For working with PDFs or webpages: extract only the most important points as individual notes, make sure you include the 'source' in the tool input
|- For follow-up requests about specific topics: create targeted notes focusing on the essential aspects of those concepts
|- For working with a complex topic: create notes that break down difficult concepts into manageable chunks
|
|What the user might say to use this tool:
|- "Help me remember that ..."
|- "Create flashcards for ..."
|- "Create rembs for ..."
|- "Add this to Rember"
|- "I want to study this later"
|- "Turn this into spaced repetition material"
|
|Here are 10 rules for writing notes to send to the Rember API.
|
|Rules:
|1. ALWAYS synthesize information to its essence, include only essential concepts or ideas by default
|2. Keep the number of notes to a minimum, create more than one note only if the user is trying to remember different concepts or ideas
|3. Include comprehensive details ONLY when the user explicitly requests them with phrases like "include all details" or "be comprehensive"
|4. Notes should be atomic and mostly focused on a single concept or idea
|5. Notes should be self-contained and make sense independently of other notes
|6. Notes should be concise, get to the point and avoid unnecessary details or verbiage
|7. Avoid repeating the same information across multiple notes
|8. Use specific attributions when referencing sources (e.g., "Researcher Name states..." not "The article suggests...")
|9. If the user asks something like "create N flashcards", explain: "I'll help you create notes on the key concepts you want to remember. Rember will automatically generate multiple flashcards from each note (typically 4-5 per note).".
|10. Follow any other user indication
`,
effect.String.stripMargin,
effect.String.trim
)
}
) {
};
var toolkit = ai.AiToolkit.empty.add(ToolCreateFlashcards);
var layerTools = toolkit.implement(
(handlers) => effect.Effect.gen(function* () {
const rember = yield* Rember;
return handlers.handle("CreateFlashcards", ({ notes, source }) => effect.pipe(
effect.Effect.gen(function* () {
const notesRember = effect.pipe(
notes,
effect.Array.map(({ text }) => ({ text: source == void 0 ? text : `${text}
${source}` }))
);
const { quantity } = yield* rember.generateCardsAndCreateRembs({ notes: notesRember });
return `${quantity} rembs have been created. The number of created flashcards is unknown, report to the user something like "I've created ${quantity} rembs in Rember, each remb contains multiple flashcards. You can review your flashcards at https://rember.com/review".`;
}),
effect.Effect.catchTag("Api/ReachedLimitUsageTracker", (_) => effect.Effect.succeed(
"The user reached the monthly limit for their Rember account. Instruct the user to visit https://rember.com/settings/account to upgrade to Rember Pro, which includes 1000 monthly rembs with high-quality AI generated flashcards."
)),
effect.Effect.catchTag("Api/ErrorReachedLimitQuantity", (_) => effect.Effect.succeed(
"You can send at most 50 notes in a single request to the Rember API"
)),
effect.Effect.catchTag("Api/ApiKeyInvalid", (_) => effect.Effect.succeed(
"The API key of the user is invalid, instruct the user to visit https://www.rember.com/settings/mcp-api and update the API key in your configs."
)),
effect.Effect.mapError((error) => new ErrorToolMCP({ message: error.message }))
));
})
);
// src/bin.ts
var apiKey = effect.pipe(
cli.Options.text("api-key"),
cli.Options.withSchema(ApiKey),
cli.Options.map(effect.Redacted.make),
cli.Options.optional
);
var command = cli.Command.make("rember-mcp", { apiKey }, ({ apiKey: apiKey2 }) => effect.pipe(
toolkit,
Effect4__namespace.flatMap(
(tools) => effect.Layer.launch(layerServerMCP({
name: "rember",
version: "1.1.3",
tools
}))
),
Effect4__namespace.provide(layerTools),
Effect4__namespace.provide(layerRember),
Effect4__namespace.provide(
effect.pipe(
effect.ConfigProvider.fromJson(effect.Option.isSome(apiKey2) ? { REMBER_API_KEY: effect.Redacted.value(apiKey2.value) } : {}),
effect.ConfigProvider.orElse(() => effect.ConfigProvider.fromEnv()),
(_) => effect.Layer.setConfigProvider(_)
)
)
));
var run = cli.Command.run(command, {
name: "Rember MCP server",
version: "1.1.3"
});
run(process.argv).pipe(
// Report errors, this needs to happen:
// - After the creation of our main layers, to report errors in the layer construction
// - Before providing `layerLogger` so that the errors are reported with the correct
// logger
// Note that we set `disableErrorReporting: true` in `NodeRuntime.runMain`.
Effect4__namespace.tapErrorCause((cause) => {
if (effect.Cause.isInterruptedOnly(cause)) {
return Effect4__namespace.void;
}
return Effect4__namespace.logError(cause);
}),
Effect4__namespace.provide(platformNode.NodeHttpClient.layerUndici),
Effect4__namespace.provide(layerLogger),
Effect4__namespace.provide(NodeContext__namespace.layer),
NodeRuntime__namespace.runMain({ disableErrorReporting: true, disablePrettyLogger: true })
);
exports.run = run;