Skip to content

feat: allow agents to go back-and-forth with AI chat in a thread #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
dist/
node_modules/
.env
db.json

# System
.DS_Store
42 changes: 42 additions & 0 deletions package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@
"name": "fern-mcp-server",
"description": "Model Context Protocol (MCP) server for the Fern API.",
"version": "0.1.0",
"type": "module",
"bin": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"start": "concurrently \"npm run build:watch\" \"npm run inspector\"",
"inspector": "npx @modelcontextprotocol/inspector@latest -- nodemon --env-file=.env -q --watch dist dist/index.js",
"build": "tsup src/index.ts --dts --clean",
"build:watch": "tsup src/index.ts --dts --watch",
"build": "tsup src/index.ts --format esm --dts --clean",
"build:watch": "tsup src/index.ts --format esm --dts --watch",
"test": "jest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"lowdb": "^7.0.1",
"uuid": "^11.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ describe("postChat", () => {
it(
"returns a non-empty string",
async () => {
const result = await postChat("What is Fern AI Chat?");
const result = await postChat([
{ role: "user", content: "What is Fern AI Chat?" },
]);
console.log({ result });

expect(result).not.toBe("");
Expand Down
9 changes: 7 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
const BASE_URL = "https://buildwithfern.com/learn";

export async function postChat(message: string) {
interface ChatMessage {
role: "user" | "assistant";
content: string;
}

export async function postChat(messages: ChatMessage[]) {
const response = await fetch(`${BASE_URL}/api/fern-docs/search/v2/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
messages: [{ role: "user", content: message }],
messages: messages,
url: "https://buildwithfern.com/learn",
filters: [],
}),
Expand Down
20 changes: 20 additions & 0 deletions src/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Low, Memory } from "lowdb";

export type Thread = Array<{
role: "user" | "assistant";
content: string;
}>;

export type Data = {
threads: Record<string, Thread>;
};

export type DB = Low<Data>;

const defaultData: Data = { threads: {} };

export function createDb() {
// TODO: Use JSONFilePreset instead
const db = new Low<Data>(new Memory(), defaultData);
return db;
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env node

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpServer, registerMcpTools } from "./mcp";
import { createMcpServer, registerMcpTools } from "./mcp.js";
import { createDb } from "./db.js";

const packageJson = require("../package.json") as any;

Expand All @@ -12,7 +13,8 @@ async function run() {
}

const server = createMcpServer(packageJson.name, packageJson.version);
registerMcpTools(server);
const db = createDb();
registerMcpTools(server, db);

const transport = new StdioServerTransport();
await server.connect(transport);
Expand Down
47 changes: 40 additions & 7 deletions src/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import * as api from "./api";
import * as api from "./api.js";
import { DB } from "./db.js";

// Create an MCP server
export function createMcpServer(name: string, version: string) {
Expand All @@ -19,15 +21,46 @@ ABOUT FERN (builtwithfern.com): Start with OpenAPI. Generate SDKs in multiple la
}

// Register MCP tools
export function registerMcpTools(server: McpServer) {
export function registerMcpTools(server: McpServer, db: DB) {
server.tool(
"ask_fern_ai",
"Ask Fern AI about anything related to Fern.",
{ message: z.string() },
async ({ message }) => {
const result = await api.postChat(message);
`Ask Fern AI about anything related to Fern.
Don't include a threadId in your initial message.
If a message requires a follow-up, include the threadId in your follow-up messages until the thread is resolved.`,
{ question: z.string(), threadId: z.string().optional() },
async ({ question, threadId: _threadId }) => {
// Issue a new thread ID if none is provided
const threadId = _threadId ?? uuidv4();
await db.read();
const thread = [...(db.data.threads[threadId] ?? [])];

// Create a user message
thread.push({ role: "user" as const, content: question });

// Post the question to the API
const answer = await api.postChat(thread);

// Create an assistant message
thread.push({ role: "assistant" as const, content: answer });

console.error(thread);

// Update the thread state
db.update(({ threads }) => {
threads[threadId] = thread;
});

// Return the answer (last message in the thread)
return {
content: [{ type: "text", text: result }],
content: [
{
type: "text",
text: JSON.stringify({
threadId: threadId,
thread: thread,
}),
},
],
};
}
);
Expand Down