Created
March 31, 2026 20:42
-
-
Save beaucarnes/9ba1644b2d1c996d31bc52cd6f27f207 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import dotenv from "dotenv"; | |
| import express from "express"; | |
| import cors from "cors"; | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | |
| import { descopeMcpAuthRouter, descopeMcpBearerAuth, DescopeMcpProvider } from "@descope/mcp-express"; | |
| import DescopeClient from "@descope/node-sdk"; | |
| import { z } from "zod"; | |
| dotenv.config(); | |
| const descopeClient = DescopeClient({ | |
| projectId: process.env.DESCOPE_PROJECT_ID, | |
| managementKey: process.env.DESCOPE_MANAGEMENT_KEY, | |
| }); | |
| async function searchWeb(query) { | |
| const response = await fetch( | |
| `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, | |
| { | |
| headers: { | |
| Accept: "application/json", | |
| "X-Subscription-Token": process.env.BRAVE_API_KEY, | |
| }, | |
| } | |
| ); | |
| if (!response.ok) { | |
| throw new Error(`Brave Search API error: ${response.status} ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| if (!data.web || !data.web.results) return []; | |
| return data.web.results.map((result) => ({ | |
| title: result.title, | |
| url: result.url, | |
| description: result.description, | |
| })); | |
| } | |
| async function saveToNotion(notionAccessToken, { title, summary, sources }) { | |
| const response = await fetch("https://api.notion.com/v1/pages", { | |
| method: "POST", | |
| headers: { | |
| Authorization: `Bearer ${notionAccessToken}`, | |
| "Content-Type": "application/json", | |
| "Notion-Version": "2022-06-28", | |
| }, | |
| body: JSON.stringify({ | |
| parent: { database_id: process.env.NOTION_DATABASE_ID }, | |
| properties: { | |
| Name: { title: [{ text: { content: title } }] }, | |
| Summary: { rich_text: [{ text: { content: summary } }] }, | |
| Sources: { url: sources }, | |
| Date: { date: { start: new Date().toISOString().split("T")[0] } }, | |
| }, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorBody = await response.text(); | |
| throw new Error(`Notion API error: ${response.status} — ${errorBody}`); | |
| } | |
| return await response.json(); | |
| } | |
| const server = new McpServer({ name: "Research Assistant", version: "1.0.0" }); | |
| server.tool( | |
| "web_search", | |
| "Search the web for information on a topic", | |
| { query: z.string().describe("The search query to look up") }, | |
| async ({ query }) => { | |
| try { | |
| const results = await searchWeb(query); | |
| if (results.length === 0) { | |
| return { content: [{ type: "text", text: `No results found for "${query}"` }] }; | |
| } | |
| const formatted = results | |
| .map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.description}`) | |
| .join("\n\n"); | |
| return { content: [{ type: "text", text: `Search results for "${query}":\n\n${formatted}` }] }; | |
| } catch (error) { | |
| return { content: [{ type: "text", text: `Search failed: ${error.message}` }] }; | |
| } | |
| } | |
| ); | |
| server.tool( | |
| "save_to_notion", | |
| "Save research findings to the team Notion database", | |
| { | |
| title: z.string().describe("Title of the research finding"), | |
| summary: z.string().describe("Summary of what was found"), | |
| sources: z.string().url().describe("Primary source URL"), | |
| }, | |
| async ({ title, summary, sources }, { authInfo }) => { | |
| try { | |
| const userId = authInfo?.sub; | |
| if (!userId) { | |
| return { content: [{ type: "text", text: "Error: No authenticated user found." }] }; | |
| } | |
| let notionTokenResponse; | |
| try { | |
| notionTokenResponse = await descopeClient.management.outboundApplication.fetchToken("notion", userId); | |
| } catch { | |
| return { content: [{ type: "text", text: "Error: Could not retrieve Notion credentials. The user may need to authorize Notion access." }] }; | |
| } | |
| const notionAccessToken = notionTokenResponse.data.token.accessToken; | |
| const result = await saveToNotion(notionAccessToken, { title, summary, sources }); | |
| return { content: [{ type: "text", text: `✅ Saved "${title}" to Notion!\nPage ID: ${result.id}\nURL: ${result.url}` }] }; | |
| } catch (error) { | |
| return { content: [{ type: "text", text: `Failed to save to Notion: ${error.message}` }] }; | |
| } | |
| } | |
| ); | |
| const app = express(); | |
| const PORT = process.env.PORT || 3000; | |
| app.use(cors({ | |
| origin: true, | |
| methods: "*", | |
| allowedHeaders: "Authorization, Origin, Content-Type, Accept, *", | |
| })); | |
| const mcpProvider = new DescopeMcpProvider({ | |
| projectId: process.env.DESCOPE_PROJECT_ID, | |
| managementKey: process.env.DESCOPE_MANAGEMENT_KEY, | |
| serverUrl: process.env.SERVER_URL, | |
| authorizationServerOptions: { | |
| isDisabled: false, | |
| enableAuthorizeEndpoint: true, | |
| enableDynamicClientRegistration: true, | |
| }, | |
| dynamicClientRegistrationOptions: { | |
| authPageUrl: `https://api.descope.com/login/${process.env.DESCOPE_PROJECT_ID}?flow=consent`, | |
| nonConfidentialClient: true, | |
| }, | |
| }); | |
| // Override protected resource metadata so clients discover our auth server | |
| app.get("/.well-known/oauth-protected-resource", (req, res) => { | |
| res.json({ | |
| resource: process.env.SERVER_URL, | |
| authorization_servers: [process.env.SERVER_URL], | |
| scopes_supported: ["openid", "profile"], | |
| bearer_methods_supported: ["header"], | |
| }); | |
| }); | |
| app.use(descopeMcpAuthRouter(undefined, mcpProvider)); | |
| app.use(["/mcp"], descopeMcpBearerAuth(mcpProvider)); | |
| const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); | |
| app.post("/mcp", express.json(), async (req, res) => { | |
| res.setHeader("X-Accel-Buffering", "no"); | |
| try { | |
| await transport.handleRequest(req, res, req.body); | |
| } catch (error) { | |
| console.error("Error handling MCP request:", error); | |
| if (!res.headersSent) { | |
| res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }); | |
| } | |
| } | |
| }); | |
| app.get("/mcp", (req, res) => { | |
| res.status(405).json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }); | |
| }); | |
| app.delete("/mcp", (req, res) => { | |
| res.status(405).json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }); | |
| }); | |
| const startServer = async () => { | |
| await server.connect(transport); | |
| app.listen(PORT, () => { | |
| console.log(`MCP server running at ${process.env.SERVER_URL}/mcp`); | |
| }); | |
| }; | |
| startServer().catch((error) => { | |
| console.error("Failed to start server:", error); | |
| process.exit(1); | |
| }); | |
| process.on("SIGINT", async () => { | |
| await transport.close(); | |
| await server.close(); | |
| process.exit(0); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment