├── .czrc ├── .gitignore ├── .nano-staged.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── biome.json ├── docs ├── .gitignore ├── app │ ├── api │ │ └── search │ │ │ └── route.ts │ ├── docs │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── favicon.ico │ ├── global.css │ ├── layout.config.tsx │ ├── layout.tsx │ └── llms.txt │ │ └── route.ts ├── components │ └── Logo.tsx ├── content │ └── docs │ │ ├── concepts │ │ ├── meta.json │ │ ├── muppet │ │ │ ├── example-bridge.ts │ │ │ ├── example-muppet.ts │ │ │ └── index.mdx │ │ ├── prompts │ │ │ ├── example-prompt.ts │ │ │ └── index.mdx │ │ ├── resources │ │ │ ├── example-completions.ts │ │ │ ├── example-dynamic.ts │ │ │ ├── example-fetcher.ts │ │ │ ├── example-static.ts │ │ │ └── index.mdx │ │ ├── tools │ │ │ ├── example-tool.ts │ │ │ └── index.mdx │ │ └── transports │ │ │ ├── index.mdx │ │ │ ├── transport-hono.ts │ │ │ ├── transport-sse.ts │ │ │ └── transport-stdio.ts │ │ ├── contributing.md │ │ ├── examples.md │ │ ├── index.mdx │ │ ├── meta.json │ │ ├── openapi │ │ ├── hono-openapi │ │ │ ├── example.ts │ │ │ └── index.mdx │ │ ├── meta.json │ │ └── openapi-specs │ │ │ ├── example.ts │ │ │ └── index.mdx │ │ ├── quickstart │ │ └── server.mdx │ │ └── runtimes │ │ ├── basic.mdx │ │ ├── bun.md │ │ ├── cloudflare-workers.md │ │ ├── deno.md │ │ └── nodejs.md ├── lib │ └── source.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public │ ├── banner.png │ ├── logo-dark.png │ └── logo-light.png ├── source.config.ts └── tsconfig.json ├── examples ├── bun-sse │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── cloudflare-workers-sse │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.jsonc ├── deno-sse │ ├── deno.json │ └── main.ts ├── deno-stdio │ ├── deno.json │ └── main.ts ├── example-forecast │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── nodejs-sse │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── with-sse-express │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── with-stdio │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── package.json ├── packages ├── core │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── basic.test.ts │ │ │ ├── openapi.test.ts │ │ │ ├── prompts.test.ts │ │ │ ├── resources.test.ts │ │ │ └── tools.test.ts │ │ ├── bridge.ts │ │ ├── describe.ts │ │ ├── index.ts │ │ ├── muppet.ts │ │ ├── openapi.ts │ │ ├── resources.ts │ │ ├── streaming.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── validator.ts │ └── tsconfig.json └── create-muppet │ ├── README.md │ ├── build.ts │ ├── package.json │ ├── src │ ├── cleanup.ts │ ├── index.ts │ ├── package-manager.ts │ ├── template.ts │ └── utils.ts │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "scopes": [ 3 | { 4 | "value": "core", 5 | "name": "core: anything core specific" 6 | } 7 | ], 8 | "scopesSearchValue": true, 9 | "allowCustomScopes": false 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .nx 4 | 5 | bin 6 | 7 | deno.lock -------------------------------------------------------------------------------- /.nano-staged.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "{apps,packages}/**/*.{js,jsx,ts,tsx,json}": (api) => 3 | `pnpm dlx @biomejs/biome check --write ${api.filenames.join(" ")}`, 4 | }; 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ****. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for showing interest in contributing to Muppet 💖, you rock! 4 | 5 | When it comes to open source, you can contribute in different ways, all of which are valuable. 6 | 7 | ## Setting up the project 8 | 9 | The repository uses pnpm workspaces and nx to manage the monorepo. Project structure is as follows: 10 | 11 | - `docs/`: Documentation for the project, built with fuma docs and nextjs. 12 | - `packages/`: Contains the core packages of the project. 13 | - `examples/`: Contains example projects that use the packages in the monorepo. These are also used as templates for the `create-muppet` CLI. 14 | 15 | To get started, make sure you have [pnpm](https://pnpm.io/) installed. Then, clone the repository and install the dependencies: 16 | 17 | ```bash 18 | pnpm install 19 | ``` 20 | 21 | To start the docs server, run: 22 | 23 | ```bash 24 | pnpm nx dev docs 25 | ``` 26 | 27 | This will start the documentation server at `http://localhost:3000`. 28 | 29 | For building the documentation for production, run: 30 | 31 | ```bash 32 | pnpm nx build docs 33 | ``` 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/core/README.md -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "style": { 11 | "noNonNullAssertion": "off" 12 | }, 13 | "suspicious": { 14 | "noExplicitAny": "warn" 15 | } 16 | } 17 | }, 18 | "formatter": { 19 | "indentStyle": "space", 20 | "lineWidth": 80, 21 | "lineEnding": "lf" 22 | }, 23 | "vcs": { 24 | "enabled": true, 25 | "clientKind": "git", 26 | "useIgnoreFile": true 27 | } 28 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from '@/lib/source'; 2 | import { createFromSource } from 'fumadocs-core/search/server'; 3 | 4 | export const { GET } = createFromSource(source); 5 | -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { 3 | DocsPage, 4 | DocsBody, 5 | DocsDescription, 6 | DocsTitle, 7 | } from "fumadocs-ui/page"; 8 | import { notFound } from "next/navigation"; 9 | import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx"; 10 | 11 | export default async function Page(props: { 12 | params: Promise<{ slug?: string[] }>; 13 | }) { 14 | const params = await props.params; 15 | const page = source.getPage(params.slug); 16 | if (!page) notFound(); 17 | 18 | const MDXContent = page.data.body; 19 | 20 | return ( 21 | 31 | {page.data.title} 32 | {page.data.description} 33 | 34 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export async function generateStaticParams() { 48 | return source.generateParams(); 49 | } 50 | 51 | export async function generateMetadata(props: { 52 | params: Promise<{ slug?: string[] }>; 53 | }) { 54 | const params = await props.params; 55 | const page = source.getPage(params.slug); 56 | if (!page) notFound(); 57 | 58 | return { 59 | title: page.data.title, 60 | description: page.data.description, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /docs/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from "fumadocs-ui/layouts/docs"; 2 | import type { ReactNode } from "react"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | import { source } from "@/lib/source"; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muppet-dev/muppet/ceec9bdadfc7db258513615e5b56c87dd2aadf18/docs/app/favicon.ico -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'fumadocs-ui/css/neutral.css'; 3 | @import 'fumadocs-ui/css/preset.css'; 4 | 5 | @source '../node_modules/fumadocs-ui/dist/**/*.js'; 6 | -------------------------------------------------------------------------------- /docs/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import { Logo } from "@/components/Logo"; 2 | import { FaBluesky, FaXTwitter, FaDiscord } from "react-icons/fa6"; 3 | import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 4 | 5 | /** 6 | * Shared layout configurations 7 | * 8 | * you can customise layouts individually from: 9 | * Home Layout: app/(home)/layout.tsx 10 | * Docs Layout: app/docs/layout.tsx 11 | */ 12 | export const baseOptions: BaseLayoutProps = { 13 | nav: { 14 | title: , 15 | }, 16 | links: [ 17 | { 18 | type: "icon", 19 | icon: , 20 | text: "X", 21 | url: "https://x.com/muppetdev", 22 | }, 23 | { 24 | type: "icon", 25 | icon: , 26 | text: "BlueSky", 27 | url: "https://bsky.app/profile/muppet.dev", 28 | }, 29 | { 30 | type: "icon", 31 | icon: , 32 | text: "Discord", 33 | url: "https://discord.gg/3fWqvErPP5", 34 | }, 35 | ], 36 | githubUrl: "https://github.com/muppet-dev/muppet", 37 | }; 38 | -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import type { Metadata } from "next"; 4 | import { Geist } from "next/font/google"; 5 | import SeoBanner from "@/public/banner.png"; 6 | import type { ReactNode } from "react"; 7 | 8 | const inter = Geist({ 9 | subsets: ["latin"], 10 | }); 11 | 12 | export function generateMetadata(): Metadata { 13 | const title = { 14 | template: "%s | muppet", 15 | default: "muppet - Toolkit for building MCPs", 16 | }; 17 | const description = 18 | "Muppet is an open toolkit which standardizes the way you build, test, and deploy your MCPs. If MCP is the USB-C port for AI applications, think of Muppet as the assembly line that produces the USB-C port."; 19 | 20 | return { 21 | title, 22 | description, 23 | keywords: [ 24 | "MCP", 25 | "MCPs", 26 | "MCP toolkit", 27 | "MCP development", 28 | "Honojs", 29 | "toolkit", 30 | "javascript", 31 | "typescript", 32 | "hono", 33 | ], 34 | metadataBase: new URL("https://muppet.dev"), 35 | category: "education", 36 | twitter: { 37 | card: "summary_large_image", 38 | title, 39 | description, 40 | images: { 41 | width: SeoBanner.width, 42 | height: SeoBanner.height, 43 | url: SeoBanner.src, 44 | }, 45 | }, 46 | openGraph: { 47 | title, 48 | description, 49 | images: { 50 | width: SeoBanner.width, 51 | height: SeoBanner.height, 52 | url: SeoBanner.src, 53 | }, 54 | siteName: "Muppet Docs", 55 | url: "/", 56 | locale: "en_US", 57 | type: "website", 58 | }, 59 | }; 60 | } 61 | 62 | export default function Layout({ children }: { children: ReactNode }) { 63 | return ( 64 | 65 | 66 | {children} 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /docs/app/llms.txt/route.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import fg from "fast-glob"; 3 | import matter from "gray-matter"; 4 | import { remark } from "remark"; 5 | import remarkGfm from "remark-gfm"; 6 | import remarkStringify from "remark-stringify"; 7 | import remarkMdx from "remark-mdx"; 8 | import { remarkInclude } from "fumadocs-mdx/config"; 9 | 10 | export const revalidate = false; 11 | 12 | export async function GET() { 13 | // all scanned content 14 | const files = await fg(["./content/docs/**/*.{md,mdx}"]); 15 | 16 | const scan = files.map(async (file) => { 17 | const fileContent = await fs.readFile(file); 18 | const { content, data } = matter(fileContent.toString()); 19 | 20 | const processed = await processContent(content); 21 | return `file: ${file} 22 | meta: ${JSON.stringify(data, null, 2)} 23 | 24 | ${processed}`; 25 | }); 26 | 27 | const scanned = await Promise.all(scan); 28 | 29 | return new Response(scanned.join("\n\n")); 30 | } 31 | 32 | async function processContent(content: string): Promise { 33 | const file = await remark() 34 | .use(remarkMdx) 35 | // https://fumadocs.vercel.app/docs/mdx/include 36 | .use(remarkInclude) 37 | // gfm styles 38 | .use(remarkGfm) 39 | // .use(your remark plugins) 40 | .use(remarkStringify) // to string 41 | .process(content); 42 | 43 | return String(file); 44 | } 45 | -------------------------------------------------------------------------------- /docs/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import MuppetLightLogo from "@/public/logo-light.png"; 4 | import MuppetDarkLogo from "@/public/logo-dark.png"; 5 | 6 | export function Logo() { 7 | return ( 8 | <> 9 | muppet logo 15 | muppet logo 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Concepts", 3 | "pages": [ 4 | "./resources/index", 5 | "./prompts/index", 6 | "./tools/index", 7 | "./muppet/index", 8 | "./transports/index" 9 | ] 10 | } -------------------------------------------------------------------------------- /docs/content/docs/concepts/muppet/example-bridge.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { Hono } from "hono"; 3 | import { muppet, bridge } from "muppet"; 4 | 5 | const app = new Hono(); 6 | 7 | // Define your tools, prompts, and resources here 8 | // ... 9 | 10 | const instance = muppet(app, { 11 | name: "My Muppet", 12 | version: "1.0.0", 13 | }).then((mcp) => { 14 | if (!mcp) { 15 | throw new Error("MCP not initialized"); 16 | } 17 | 18 | bridge({ 19 | mcp, 20 | transport: new StdioServerTransport(), 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/muppet/example-muppet.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { muppet } from "muppet"; 3 | 4 | const app = new Hono(); 5 | 6 | // Define your tools, prompts, and resources here 7 | // ... 8 | 9 | const instance = muppet(app, { 10 | name: "My Muppet", 11 | version: "1.0.0", 12 | }); 13 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/muppet/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MCP & Bridge 3 | description: Creating the MCP instance of your app 4 | --- 5 | 6 | import { TypeTable } from 'fumadocs-ui/components/type-table'; 7 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 8 | import fs from 'fs'; 9 | 10 | ## Muppet 11 | 12 | Now that you have defined your tools and prompts, you can create the MCP instance of your app. This is done using the `muppet` function. This function takes the `app` instance and an configurtions object as arguments. 13 | 14 | 15 | 16 | The `muppet` function takes the following configurations 17 | 18 | ", 36 | }, 37 | events: { 38 | type: "Emitter" 39 | } 40 | }} 41 | /> 42 | 43 | ## Bridge 44 | 45 | Now let's create a bridge between the LLM and your server. This is done using the `bridge` function. Think of this like [Bifröst](https://en.wikipedia.org/wiki/Bifr%C3%B6st), it connects the LLM using the [transport layer](/docs/concepts/transports) to your server. 46 | 47 | This takes in your muppet instance and the transport layer you wanna use. 48 | 49 | 50 | 51 | You can check out other transport layers [here](/docs/concepts/transports). -------------------------------------------------------------------------------- /docs/content/docs/concepts/prompts/example-prompt.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { describePrompt, mValidator, type PromptResponseType } from "muppet"; 3 | import z from "zod"; 4 | 5 | const app = new Hono(); 6 | 7 | app.post( 8 | "/explain-like-im-5", 9 | describePrompt({ 10 | name: "Explain like I'm 5", 11 | description: "A prompt to explain an advance topic to a 5 year old", 12 | completion: ({ name, value }) => [ 13 | "quantum physics", 14 | "machine learning", 15 | "natural language processing", 16 | "artificial intelligence", 17 | ], 18 | }), 19 | mValidator( 20 | "json", 21 | z.object({ 22 | topic: z.string(), 23 | }), 24 | ), 25 | (c) => { 26 | const { topic } = c.req.valid("json"); 27 | return c.json([ 28 | { 29 | role: "user", 30 | content: { 31 | type: "text", 32 | text: `Explain ${topic} to me like I'm five`, 33 | }, 34 | }, 35 | ]); 36 | }, 37 | ); 38 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/prompts/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Prompts 3 | description: Create reusable prompt templates and workflows 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | You can create reusable prompt templates, this not only allows you to reuse the same prompt in multiple places, but also thanks to mcps you can make then dynamic and share them with others. For eg, you can predefine prompts like **"Explain \{topic\} to me like I'm five"**, here `{topic}` is a variable that can be replaced with any value. 10 | 11 | You can learn more about prompts in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/prompts). 12 | 13 | 14 | 15 | `mValidator` supports Standard Schema, which means you can use validation libs which supports Standard Schema like zod, valibot, arktype, typebox, etc. 16 | 17 | You can define completions using `completion` prop for your prompts. This allows you to provide the LLM with a list of possible values for the variable in the prompt. For example, if you have a prompt that asks for a city name, you can provide a list of cities as completions. -------------------------------------------------------------------------------- /docs/content/docs/concepts/resources/example-completions.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { registerResources } from "muppet"; 3 | 4 | const app = new Hono(); 5 | 6 | app.post( 7 | "/documents", 8 | registerResources((c) => { 9 | return [ 10 | { 11 | type: "template", // This tells muppet that this is a dynamic resource 12 | uri: "https://lorem.{value}", 13 | name: "Todo list", 14 | mimeType: "text/plain", 15 | completion: ({ name, value }) => { 16 | return ["muppet", "hono", "mcps"]; 17 | }, 18 | }, 19 | ]; 20 | }), 21 | ); 22 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/resources/example-dynamic.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { registerResources } from "muppet"; 3 | 4 | const app = new Hono(); 5 | 6 | app.post( 7 | "/documents", 8 | registerResources((c) => { 9 | return [ 10 | { 11 | type: "template", // This tells muppet that this is a dynamic resource 12 | uri: "https://lorem.{ending}", 13 | name: "Todo list", 14 | mimeType: "text/plain", 15 | }, 16 | ]; 17 | }), 18 | ); 19 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/resources/example-fetcher.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { muppet } from "muppet"; 3 | 4 | const app = new Hono(); 5 | 6 | muppet(app, { 7 | name: "example-fetcher", 8 | version: "0.0.1", 9 | resources: { 10 | https: (uri) => { 11 | return [ 12 | { 13 | uri, 14 | text: "Todo list", 15 | mimeType: "text/plain", 16 | }, 17 | ]; 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/resources/example-static.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { registerResources } from "muppet"; 3 | 4 | const app = new Hono(); 5 | 6 | app.post( 7 | "/documents", 8 | registerResources((c) => { 9 | return [ 10 | { 11 | uri: "https://lorem.ipsum", 12 | name: "Todo list", 13 | mimeType: "text/plain", 14 | }, 15 | ]; 16 | }), 17 | ); 18 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/resources/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources 3 | description: Expose data and content from your servers to LLMs 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | You can think of resources as a way to expose data and content from your servers to LLMs. Resources are a powerful way to provide LLMs with access to structured data, allowing them to make informed decisions and generate more accurate responses. 10 | 11 | Resources can be used to expose data from databases, APIs, or any other data source. They can also be used to expose static content, such as HTML pages or JSON files. 12 | 13 | You can learn more about resources in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/resources). 14 | 15 | ## Dynamic Resources 16 | 17 | URIs of these resources are generated dynamically based on the request. This allows you to create resources that can be customized based on the input provided by the LLM. 18 | 19 | URIs of these resources typically looks like this `[protocol]:[path]/{[variable]}`, an example of this would be `muppet:forecast/{city}`. In this case, the `city` variable is replaced with the value provided by the LLM. 20 | 21 | The protocol is used to identify the fetcher that would be used to get data. You can see the example to define the fetcher [here](). 22 | 23 | 24 | 25 | You can also define completions for dynamic resources. This allows you to provide the LLM with a list of possible values for the variable in the URI. For example, if you have a resource that fetches weather data for a specific city, you can provide a list of cities as completions. Here is an example of how to define completions for a dynamic resource 26 | 27 | 28 | 29 | ## Static Resources 30 | 31 | Unlike dynamic resources, static resources have a fixed URI. This means that the resource is always available at the same location, regardless of the input provided by the LLM. The also don't have the `completion` function. 32 | 33 | 34 | 35 | ## Defining Resource Fetcher 36 | 37 | You can define a fetcher for your resource using the `resources` prop in your `muppet` instance. The fetcher is a function that takes the URI of the resource and returns the data for that resource. The fetcher can be an async function, which allows you to fetch data from APIs or databases. 38 | 39 | 40 | 41 | Fetcher may return multiple resources in response. This could be used, for example, to return a list of files inside a directory when the directory is read. -------------------------------------------------------------------------------- /docs/content/docs/concepts/tools/example-tool.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { type ToolResponseType, describeTool, mValidator } from "muppet"; 3 | import z from "zod"; 4 | 5 | const app = new Hono(); 6 | 7 | app.post( 8 | "/hello", 9 | describeTool({ 10 | name: "Hello World", 11 | description: "A simple hello world tool", 12 | }), 13 | mValidator( 14 | "json", 15 | z.object({ 16 | name: z.string(), 17 | }), 18 | ), 19 | (c) => { 20 | const { name } = c.req.valid("json"); 21 | return c.json([ 22 | { 23 | type: "text", 24 | text: `Hello ${name}!`, 25 | }, 26 | ]); 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/tools/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tools 3 | description: Enable LLMs to perform actions through your server 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | You can think of tools as a way to enable LLMs to perform actions through your server. Tools are a powerful way to provide LLMs with access to external APIs, allowing them to perform actions and retrieve data from those APIs. These can range from doing simple task like fetching data from an API to more complex tasks like ordering food or booking a flight. 10 | 11 | You can learn more about tools in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/tools). 12 | 13 | 14 | 15 | `mValidator` supports Standard Schema, which means you can use validation libs which supports Standard Schema like zod, valibot, arktype, typebox, etc. -------------------------------------------------------------------------------- /docs/content/docs/concepts/transports/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Transports 3 | description: Connect your server to LLMs 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | Transports basically defines how the messages will be shared between the LLMs and your server. We use `bridge` function to connect the muppet instance with the transport layer. You can learn more about the `bridge` function [here](/docs/concepts/muppet#bridge) 10 | 11 | Examples for all these transports are available [here](/docs/examples). You can learn more about transports in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/transports). 12 | 13 | ## Stdio Transport 14 | 15 | The `StdioServerTransport` is a transport layer that allows you to connect your server to LLMs using standard input and output streams. This transport layer is useful for running your server in environments where you don't have access to a network, such as in a local development environment or in a container or a private network. 16 | 17 | 18 | 19 | This does blocks your stdio streams, so you won't be able to use `console.log` or other stdio functions while the server is running. `muppet` has a built-in logger that you can use to log messages. You can use the `logger` prop to set the logger for your server. It uses `pino` under the hood, so you can use any `pino` compatible logger. 20 | 21 | ```ts 22 | muppet(app, { 23 | logger: pino( 24 | pino.destination( 25 | "./muppet.log", 26 | ), 27 | ) 28 | }) 29 | ``` 30 | 31 | This will store all the logs in the `muppet.log` file. 32 | 33 | ## SSE Transport 34 | 35 | The `SSEServerTransport` is a transport layer that allows you to connect your server to LLMs using Server-Sent Events (SSE). This transport layer only runs in `nodejs` runtime. 36 | 37 | 38 | 39 | There is a better way, which is to use [`SSEHonoTransport`](#sse-hono-transport) which runs on all runtimes which support streaming with hono. 40 | 41 | ## SSE Hono Transport 42 | 43 | This transport layer is similar to `SSEServerTransport`, but it uses the `hono`'s streaming capabilities to send the response. This makes running this transport layer on all runtimes which support streaming, such as `nodejs`, `deno`, `bun`, and `cloudflare workers`. You can learn more about this in the [hono documentation](https://hono.dev/docs/helpers/streaming). 44 | 45 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/transports/transport-hono.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { Hono } from "hono"; 3 | import { bridge, muppet } from "muppet"; 4 | import { SSEHonoTransport, streamSSE } from "muppet/streaming"; 5 | 6 | const app = new Hono(); 7 | 8 | // Define your tools, prompts, and resources here 9 | // ... 10 | 11 | const mcp = muppet(app, { 12 | name: "My Muppet", 13 | version: "1.0.0", 14 | }); 15 | 16 | let transport: SSEHonoTransport | null = null; 17 | 18 | const server = new Hono(); 19 | 20 | server.get("/sse", async (c) => { 21 | return streamSSE(c, async (stream) => { 22 | transport = new SSEHonoTransport("/messages"); 23 | transport.connectWithStream(stream); 24 | 25 | await bridge({ 26 | mcp, 27 | transport, 28 | }); 29 | }); 30 | }); 31 | 32 | server.post("/messages", async (c) => { 33 | if (!transport) { 34 | throw new Error("Transport not initialized"); 35 | } 36 | 37 | await transport.handlePostMessage(c); 38 | return c.text("ok"); 39 | }); 40 | 41 | server.onError((err, c) => { 42 | console.error(err); 43 | return c.body(err.message, 500); 44 | }); 45 | 46 | serve( 47 | { 48 | fetch: server.fetch, 49 | port: 3001, 50 | }, 51 | (info) => { 52 | console.log(`Server started at http://localhost:${info.port}`); 53 | }, 54 | ); 55 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/transports/transport-sse.ts: -------------------------------------------------------------------------------- 1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 2 | import { Hono } from "hono"; 3 | import { bridge, muppet } from "muppet"; 4 | import express from "express"; 5 | 6 | const app = new Hono(); 7 | 8 | // Define your tools, prompts, and resources here 9 | // ... 10 | 11 | const mcp = muppet(app, { 12 | name: "My Muppet", 13 | version: "1.0.0", 14 | }); 15 | 16 | let transport: SSEServerTransport | null = null; 17 | 18 | const server = express().use((req, res, next) => { 19 | console.log("Request received", req.url); 20 | next(); 21 | }); 22 | 23 | server.get("/sse", async (req, res) => { 24 | transport = new SSEServerTransport("/messages", res); 25 | 26 | bridge({ 27 | mcp, 28 | transport, 29 | }); 30 | }); 31 | 32 | server.post("/messages", (req, res) => { 33 | if (transport) { 34 | transport.handlePostMessage(req, res); 35 | } 36 | }); 37 | 38 | server.listen(3001); 39 | -------------------------------------------------------------------------------- /docs/content/docs/concepts/transports/transport-stdio.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { Hono } from "hono"; 3 | import { bridge, muppet } from "muppet"; 4 | 5 | const app = new Hono(); 6 | 7 | // Define your tools, prompts, and resources here 8 | // ... 9 | 10 | bridge({ 11 | mcp: muppet(app, { 12 | name: "My Muppet", 13 | version: "1.0.0", 14 | }), 15 | transport: new StdioServerTransport(), 16 | }); 17 | -------------------------------------------------------------------------------- /docs/content/docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | description: How to participate in Muppet development 4 | --- 5 | 6 | We welcome contributions from the community! Please review our [contributing guidelines](https://github.com/muppet-dev/muppet/blob/main/CONTRIBUTING.md) for details on how to submit changes. 7 | 8 | All contributors must adhere to our [Code of Conduct](https://github.com/muppet-dev/muppet/blob/main/CODE_OF_CONDUCT.md). 9 | 10 | For questions and discussions, please use [GitHub Discussions](https://github.com/orgs/muppet-dev/discussions). 11 | -------------------------------------------------------------------------------- /docs/content/docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Servers 3 | description: A list of example servers built with Muppet 4 | --- 5 | 6 | This page showcases a few example servers built with Muppet. These examples are designed to help you understand how to use Muppet in different scenarios and runtimes, and provide a starting point for your own projects. 7 | 8 | - [with-stdio](https://github.com/muppet-dev/muppet/tree/main/examples/with-stdio) - Runs in Node.js and uses `StdioServerTransport` transport layer from `@modelcontextprotocol/sdk` to connect. 9 | - [example-forecast](https://github.com/muppet-dev/muppet/tree/main/examples/example-forecast) - A simple weather server that exposes two tools: `get-alerts` and `get-forecast`, which can be used to fetch weather alerts and forecasts, respectively. 10 | - [with-sse-express](https://github.com/muppet-dev/muppet/tree/main/examples/with-sse-express) - Runs in Node.js and uses `SseServerTransport` transport layer from `@modelcontextprotocol/sdk` to connect. 11 | - [with-sse](https://github.com/muppet-dev/muppet/tree/main/examples/with-sse) - Uses `SSEHonoTransport` transport layer from `muppet/streaming` to connect and can be used with all the runtimes which support Streaming with hono. More details about that [here](https://hono.dev/docs/helpers/streaming). This one runs on Node.js. 12 | - [with-edge](https://github.com/muppet-dev/muppet/tree/main/examples/with-edge) - Same as `with-ss` example but runs on Cloudflare Workers. 13 | -------------------------------------------------------------------------------- /docs/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Streamlining Your MCP Development 4 | --- 5 | 6 | Muppet is an open-source toolkit designed to simplify the entire lifecycle of your Model Context Protocol (MCP) servers—from development and testing to deployment. 7 | 8 | ![Muppet Banner](../../public/banner.png) 9 | 10 | ## Why Choose Muppet? 11 | 12 | Muppet stands out by seamlessly integrating with the [Hono](https://hono.dev/) web framework. This integration allows you to enhance your existing Hono applications with MCP capabilities without the need for extensive rewrites. Muppet functions as a natural extension of Hono, providing an intuitive development experience. 13 | 14 | ## Key Use Cases 15 | 16 | - **Enhance Existing Hono Applications:** Transform your current Hono app into a fully functional MCP server. 17 | - **Develop New MCP Servers:** Build new MCP servers from scratch using Hono's robust framework. 18 | - **OpenAPI-Based Development:** Create MCP servers guided by OpenAPI specifications for standardized API design. 19 | 20 | ## Advantages of Muppet 21 | 22 | By leveraging Hono's features, Muppet enables you to incorporate essential functionalities into your MCP servers, such as: 23 | 24 | - **Authentication:** Secure your MCP endpoints using Hono's authentication middleware. 25 | - **CORS Management:** Effortlessly handle cross-origin requests. 26 | - **Rate Limiting:** Protect your resources and ensure fair usage with built-in rate limiting. 27 | - **Logging:** Monitor requests and responses using Hono's logging capabilities or integrate with third-party logging libraries. 28 | - **Payment Integration:** Seamlessly incorporate billing and payment processes into your MCP workflows. 29 | 30 | These features become increasingly valuable as your project scales, providing a robust foundation for efficient MCP management. 31 | 32 | *Note: Muppet draws inspiration from the [hono-openapi](https://www.npmjs.com/package/hono-openapi) package. If you're familiar with `hono-openapi`, you'll find Muppet's design and philosophy comfortably aligned.* 33 | 34 | ## Getting Started 35 | 36 | Select the path that aligns with your development goals: 37 | 38 | ### Quick Start Guides 39 | 40 | 41 | 42 | 43 | 44 | ## Core Capabilities 45 | 46 | Explore how to implement common functionalities with Muppet: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ## Contributing 56 | 57 | We welcome contributions! Please refer to our [Contributing Guide](/docs/contributing) to learn how you can help improve Muppet. 58 | 59 | ## Support and Feedback 60 | 61 | We're here to assist and value your feedback: 62 | 63 | - **Bug Reports & Feature Requests:** [Create a GitHub issue](https://github.com/muppet-dev) for any issues or suggestions regarding the Muppet SDKs or documentation. 64 | - **MCP Specification Discussions:** Join the [specification discussions](https://github.com/orgs/muppet-dev/discussions) for general discussions and Q&A about the Model Context Protocol. 65 | -------------------------------------------------------------------------------- /docs/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "muppet", 3 | "pages": [ 4 | "index", 5 | "./quickstart", 6 | "./runtimes", 7 | "examples", 8 | "./concepts", 9 | "./openapi", 10 | "contributing" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/content/docs/openapi/hono-openapi/example.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { describeRoute, uniqueSymbol } from "hono-openapi"; 3 | import { muppet, mValidator } from "muppet"; 4 | import z from "zod"; 5 | 6 | const app = new Hono(); 7 | 8 | app.get( 9 | "/products", 10 | describeRoute({ 11 | summary: "Get all products", 12 | description: "This endpoint returns a list of all products.", 13 | }), 14 | // TODO: Change this with the hono-openapi validator 15 | mValidator("query", z.object({ page: z.number().optional() })), 16 | async (c) => { 17 | return c.json([ 18 | { 19 | id: 1, 20 | name: "Product 1", 21 | price: 10.0, 22 | }, 23 | { 24 | id: 2, 25 | name: "Product 2", 26 | price: 20.0, 27 | }, 28 | ]); 29 | }, 30 | ); 31 | 32 | muppet(app, { 33 | name: "hono-openapi-mcp", 34 | version: "0.0.1", 35 | // By passing this, muppet will scan the hono-openapi middlewares too 36 | symbols: [uniqueSymbol], 37 | }); 38 | -------------------------------------------------------------------------------- /docs/content/docs/openapi/hono-openapi/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Hono OpenAPI 3 | description: Reuse your hono-openapi validators to generate MCP servers 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | This requires `hono-openapi@0.5.0` or later to work. You can checkout the project [here](https://github.com/rhinobase/hono-openapi) 10 | 11 | `muppet` and `hono-openapi` both uses a validator supporting [Standard Schema](https://standardschema.dev/) to understand the structure of the request and response. Now with latest release of `hono-openapi`, you can use the same validator inplace of the one provided by `muppet` to generate the MCP server. This is useful if you are already using `hono-openapi` in your project and want to reuse the same validators for your MCP server. 12 | 13 | -------------------------------------------------------------------------------- /docs/content/docs/openapi/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "OpenAPI" 3 | } -------------------------------------------------------------------------------- /docs/content/docs/openapi/openapi-specs/example.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { bridge } from "muppet"; 3 | import { fromOpenAPI } from "muppet/openapi"; 4 | 5 | const mcp = fromOpenAPI({ 6 | // Add your OpenAPI spec here 7 | info: { 8 | title: "My Muppet", 9 | version: "1.0.0", 10 | }, 11 | openapi: "3.1.0", 12 | paths: {}, 13 | }); 14 | 15 | bridge({ 16 | mcp, 17 | transport: new StdioServerTransport(), 18 | }); 19 | -------------------------------------------------------------------------------- /docs/content/docs/openapi/openapi-specs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using OpenAPI Specs 3 | description: Generate MCP servers from OpenAPI Specs 4 | --- 5 | 6 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; 7 | import fs from 'fs'; 8 | 9 | You can create an MCP server from OpenAPI specs using the `fromOpenAPI` function. This function takes in the OpenAPI specs and generates the MCP server from it. This is useful if you have an existing OpenAPI spec, from any codebase and want to generate a MCP server from it. 10 | 11 | 12 | 13 | 14 | This will create all the routes as tools for the MCP server. Currently you cannot change this behavior but we are working on improving this in the future. 15 | -------------------------------------------------------------------------------- /docs/content/docs/quickstart/server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: For Server Developers 3 | description: Get started building your own server to use in Claude for Desktop and other clients. 4 | --- 5 | 6 | import { Step, Steps } from 'fumadocs-ui/components/steps'; 7 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 8 | 9 | In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases. 10 | 11 | This is the same tutorial as the one [here](https://modelcontextprotocol.io/quickstart/server) from the Model Context Protocol documentation. You can compare the two to see how Muppet simplifies the process of building and deploying MCPs. 12 | 13 | ## What we'll be building 14 | 15 | Many LLMs do not currently have the ability to fetch the forecast and severe weather alerts. Let's use MCP to solve that! 16 | 17 | We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude for Desktop): 18 | 19 | {/* TODO: Add images */} 20 | 21 | ### Core MCP Concepts 22 | 23 | MCP servers can provide three main types of capabilities: 24 | 25 | 1. **Resources**: File-like data that can be read by clients (like API responses or file contents) 26 | 2. **Tools**: Functions that can be called by the LLM (with user approval) 27 | 3. **Prompts**: Pre-written templates that help users accomplish specific tasks 28 | 29 | This tutorial will primarily focus on tools. 30 | 31 | ### Building our server 32 | 33 | Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/muppet-dev/muppet/tree/main/examples/example-forecast) 34 | 35 | 36 | 37 | 38 | 1. Let's create and setup our project 39 | 40 | ```bash 41 | # Initialize a new hono project, here we are using nodejs but you can use any other template 42 | pnpm create hono@latest my-mcp -i -t nodejs -p pnpm 43 | 44 | # Install dependencies 45 | pnpm add muppet @modelcontextprotocol/sdk @hono/standard-validator 46 | 47 | # You can use any validation lib that supports Standard Schema 48 | pnpm add zod 49 | ``` 50 | 51 | 52 | 2. Importing packages 53 | 54 | Add these to the top of your `src/index.ts` 55 | 56 | ```ts 57 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 58 | import { Hono } from 'hono'; 59 | import { 60 | type ToolResponseType, 61 | bridge, 62 | describeTool, 63 | mValidator, 64 | muppet, 65 | } from "muppet"; 66 | import z from "zod"; 67 | ``` 68 | 69 | 70 | 3. Helper functions 71 | 72 | Next, let's add our helper functions for querying and formatting the data from the National Weather Service API 73 | 74 | ```ts 75 | const NWS_API_BASE = "https://api.weather.gov"; 76 | const USER_AGENT = "weather-app/1.0"; 77 | 78 | // Helper function for making NWS API requests 79 | async function makeNWSRequest(url: string): Promise { 80 | const headers = { 81 | "User-Agent": USER_AGENT, 82 | Accept: "application/geo+json", 83 | }; 84 | 85 | try { 86 | const response = await fetch(url, { headers }); 87 | if (!response.ok) { 88 | throw new Error(`HTTP error! status: ${response.status}`); 89 | } 90 | return (await response.json()) as T; 91 | } catch (error) { 92 | console.error("Error making NWS request:", error); 93 | return null; 94 | } 95 | } 96 | 97 | interface AlertFeature { 98 | properties: { 99 | event?: string; 100 | areaDesc?: string; 101 | severity?: string; 102 | status?: string; 103 | headline?: string; 104 | }; 105 | } 106 | 107 | // Format alert data 108 | function formatAlert(feature: AlertFeature): string { 109 | const props = feature.properties; 110 | return [ 111 | `Event: ${props.event || "Unknown"}`, 112 | `Area: ${props.areaDesc || "Unknown"}`, 113 | `Severity: ${props.severity || "Unknown"}`, 114 | `Status: ${props.status || "Unknown"}`, 115 | `Headline: ${props.headline || "No headline"}`, 116 | "---", 117 | ].join("\n"); 118 | } 119 | 120 | interface ForecastPeriod { 121 | name?: string; 122 | temperature?: number; 123 | temperatureUnit?: string; 124 | windSpeed?: string; 125 | windDirection?: string; 126 | shortForecast?: string; 127 | } 128 | 129 | interface AlertsResponse { 130 | features: AlertFeature[]; 131 | } 132 | 133 | interface PointsResponse { 134 | properties: { 135 | forecast?: string; 136 | }; 137 | } 138 | 139 | interface ForecastResponse { 140 | properties: { 141 | periods: ForecastPeriod[]; 142 | }; 143 | } 144 | ``` 145 | 146 | 147 | 4. Implementing tool execution 148 | 149 | Now let's implement the tool execution logic. This is where we define how our tools will work. 150 | 151 | ```ts 152 | const app = new Hono(); 153 | 154 | // Define the get-alerts tool 155 | app.post( 156 | "/get-alerts", 157 | describeTool({ 158 | name: "get-alerts", 159 | description: "Get weather alerts for a state", 160 | }), 161 | mValidator( 162 | "json", 163 | z.object({ 164 | state: z 165 | .string() 166 | .length(2) 167 | .describe("Two-letter state code (e.g. CA, NY)"), 168 | }), 169 | ), 170 | async (c) => { 171 | const { state } = c.req.valid("json"); 172 | 173 | const stateCode = state.toUpperCase(); 174 | const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; 175 | const alertsData = await makeNWSRequest(alertsUrl); 176 | 177 | if (!alertsData) { 178 | return c.json({ 179 | content: [ 180 | { 181 | type: "text", 182 | text: "Failed to retrieve alerts data", 183 | }, 184 | ], 185 | }); 186 | } 187 | 188 | const features = alertsData.features || []; 189 | if (features.length === 0) { 190 | return c.json({ 191 | content: [ 192 | { 193 | type: "text", 194 | text: `No active alerts for ${stateCode}`, 195 | }, 196 | ], 197 | }); 198 | } 199 | 200 | const formattedAlerts = features.map(formatAlert); 201 | const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`; 202 | 203 | return c.json({ 204 | content: [ 205 | { 206 | type: "text", 207 | text: alertsText, 208 | }, 209 | ], 210 | }); 211 | }, 212 | ); 213 | 214 | app.post( 215 | "/get-forecast", 216 | describeTool({ 217 | name: "get-forecast", 218 | description: "Get weather forecast for a location", 219 | }), 220 | mValidator( 221 | "json", 222 | z.object({ 223 | latitude: z 224 | .number() 225 | .min(-90) 226 | .max(90) 227 | .describe("Latitude of the location"), 228 | longitude: z 229 | .number() 230 | .min(-180) 231 | .max(180) 232 | .describe("Longitude of the location"), 233 | }), 234 | ), 235 | async (c) => { 236 | const { latitude, longitude } = c.req.valid("json"); 237 | 238 | // Get grid point data 239 | const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; 240 | const pointsData = await makeNWSRequest(pointsUrl); 241 | 242 | if (!pointsData) { 243 | return c.json({ 244 | content: [ 245 | { 246 | type: "text", 247 | text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, 248 | }, 249 | ], 250 | }); 251 | } 252 | 253 | const forecastUrl = pointsData.properties?.forecast; 254 | if (!forecastUrl) { 255 | return c.json({ 256 | content: [ 257 | { 258 | type: "text", 259 | text: "Failed to get forecast URL from grid point data", 260 | }, 261 | ], 262 | }); 263 | } 264 | 265 | // Get forecast data 266 | const forecastData = await makeNWSRequest(forecastUrl); 267 | if (!forecastData) { 268 | return c.json({ 269 | content: [ 270 | { 271 | type: "text", 272 | text: "Failed to retrieve forecast data", 273 | }, 274 | ], 275 | }); 276 | } 277 | 278 | const periods = forecastData.properties?.periods || []; 279 | if (periods.length === 0) { 280 | return c.json({ 281 | content: [ 282 | { 283 | type: "text", 284 | text: "No forecast periods available", 285 | }, 286 | ], 287 | }); 288 | } 289 | 290 | // Format forecast periods 291 | const formattedForecast = periods.map((period: ForecastPeriod) => 292 | [ 293 | `${period.name || "Unknown"}:`, 294 | `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`, 295 | `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`, 296 | `${period.shortForecast || "No forecast available"}`, 297 | "---", 298 | ].join("\n"), 299 | ); 300 | 301 | const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`; 302 | 303 | return c.json({ 304 | content: [ 305 | { 306 | type: "text", 307 | text: forecastText, 308 | }, 309 | ], 310 | }); 311 | }, 312 | ); 313 | 314 | 315 | 5. Running the server 316 | 317 | Finally, implement the main function to run the server 318 | 319 | ```ts 320 | muppet(app, { 321 | name: "weather", 322 | version: "1.0.0", 323 | }).then((mcp) => { 324 | if (!mcp) { 325 | throw new Error("MCP not initialized"); 326 | } 327 | 328 | // Bridge the mcp with the transport 329 | bridge({ 330 | mcp, 331 | transport: new StdioServerTransport(), 332 | }); 333 | }); 334 | ``` 335 | 336 | Now depending upon the client you can either build the server or run it directly in dev mode. For simplicity we will just build the server. The command for this will depend on the runtime you are using. For example, if you are using nodejs, you can follow these steps 337 | 338 | ```json 339 | { 340 | "scripts": { 341 | "build": "esbuild --bundle --minify --platform=node --outfile=./build/index.js ./src/index.ts", 342 | }, 343 | "dependencies": { 344 | "esbuild": "^0.24.2" 345 | } 346 | } 347 | ``` 348 | 349 | Let's now test your server from an existing MCP host, Claude for Desktop. 350 | 351 | 352 | 353 | ## Connection with Claude for Desktop 354 | 355 | First, make sure you have Claude for Desktop installed. [You can install the latest version 356 | here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.** 357 | 358 | We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist. 359 | 360 | For example, if you have [VS Code](https://code.visualstudio.com/) installed: 361 | 362 | 363 | 364 | ```bash 365 | code ~/Library/Application\ Support/Claude/claude_desktop_config.json 366 | ``` 367 | 368 | 369 | ```powershell 370 | code $env:AppData\Claude\claude_desktop_config.json 371 | ``` 372 | 373 | 374 | 375 | You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured. 376 | 377 | In this case, we'll add our single weather server like so: 378 | 379 | 380 | 381 | ```json 382 | { 383 | "mcpServers": { 384 | "weather": { 385 | "command": "node", 386 | "args": [ 387 | "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js" 388 | ] 389 | } 390 | } 391 | } 392 | ``` 393 | 394 | 395 | ```json 396 | { 397 | "mcpServers": { 398 | "weather": { 399 | "command": "node", 400 | "args": [ 401 | "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js" 402 | ] 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | 409 | 410 | This tells Claude for Desktop: 411 | 1. There's an MCP server named "weather" 412 | 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js` 413 | 414 | Save the file, and restart **Claude for Desktop**. 415 | 416 | ## Test with commands 417 | 418 | For this you can follow the instructions [here](https://modelcontextprotocol.io/quickstart/server#test-with-commands) to connect your server to the client. 419 | -------------------------------------------------------------------------------- /docs/content/docs/runtimes/basic.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Setup 3 | --- 4 | 5 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 6 | 7 | Setting up Muppet is super easy. We can set up the project, write code, develop with a local server, and deploy quickly. The same code will work on any runtime, just with different entry points. Let's look at the basic usage of Muppet. 8 | 9 | ## Starter 10 | 11 | Starter templates are available for each platform. Use the following `create-muppet` command. 12 | 13 | ```bash tab="npm" 14 | npm create muppet@latest my-app 15 | ``` 16 | 17 | ```bash tab="yarn" 18 | yarn create muppet my-app 19 | ``` 20 | 21 | ```bash tab="pnpm" 22 | pnpm create muppet@latest my-app 23 | ``` 24 | 25 | ```bash tab="bun" 26 | bun create muppet@latest my-app 27 | ``` 28 | 29 | ```bash tab="deno" 30 | deno init --npm muppet@latest my-app 31 | ``` 32 | 33 | Then you will be asked which transport layer and runtime you would like to use. The template will be pulled into `my-app`, so go to it and install the dependencies. 34 | 35 | ```bash tab="npm" 36 | cd my-app 37 | npm i 38 | ``` 39 | 40 | ```bash tab="yarn" 41 | cd my-app 42 | yarn 43 | ``` 44 | 45 | ```bash tab="pnpm" 46 | cd my-app 47 | pnpm i 48 | ``` 49 | 50 | ```bash tab="bun" 51 | cd my-app 52 | bun i 53 | ``` 54 | 55 | Once the package installation is complete, run the following command to start up a local server. 56 | 57 | ```bash tab="npm" 58 | npm run dev 59 | ``` 60 | 61 | ```bash tab="yarn" 62 | yarn dev 63 | ``` 64 | 65 | ```bash tab="pnpm" 66 | pnpm dev 67 | ``` 68 | 69 | ```bash tab="bun" 70 | bun run dev 71 | ``` 72 | 73 | ## Next Step 74 | 75 | Now that you have a basic setup, you can start writing your code. The starter template will have a basic structure for you to follow. You can start by modifying the `src/index.ts` file and adding your own tools. 76 | 77 | The final `export default` parts may vary from runtime to runtime, but all of the application code will run the same code everywhere. -------------------------------------------------------------------------------- /docs/content/docs/runtimes/bun.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bun 3 | --- 4 | 5 | With [Bun](https://bun.sh) you can run Muppet on both the transport layers. 6 | 7 | For the SSE transport layer, you will have to create a proxy server to handle the SSE connection. Here is an example of how to do that: 8 | 9 | ```ts 10 | let transport: SSEHonoTransport | null = null; 11 | 12 | const server = new Hono(); 13 | 14 | server.get("/sse", (c) => { 15 | return streamSSE(c, async (stream) => { 16 | transport = new SSEHonoTransport("/messages"); 17 | transport.connectWithStream(stream); 18 | 19 | await bridge({ 20 | mcp, 21 | transport, 22 | }); 23 | }); 24 | }); 25 | 26 | server.post("/messages", async (c) => { 27 | if (!transport) { 28 | throw new Error("Transport not initialized"); 29 | } 30 | 31 | await transport.handlePostMessage(c); 32 | return c.text("ok"); 33 | }); 34 | 35 | server.onError((err, c) => { 36 | console.error(err); 37 | return c.body(err.message, 500); 38 | }); 39 | 40 | export default server; 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/content/docs/runtimes/cloudflare-workers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloudflare Workers 3 | --- 4 | 5 | For connecting your MCP with a LLM, you will need to pressists the transport instance. On a server it is quite easy, but on a worker you will need to use Durable Objects to achieve that. Here is an example of how to do that: 6 | 7 | ```ts 8 | const server = new Hono<{ Bindings: { transport: SSEHonoTransport } }>(); 9 | 10 | server.get("/sse", (c) => { 11 | return streamSSE(c, async (stream) => { 12 | c.env.transport.connectWithStream(stream); 13 | 14 | await bridge({ 15 | mcp, 16 | transport: c.env.transport, 17 | }); 18 | }); 19 | }); 20 | 21 | server.post("/messages", async (c) => { 22 | const transport = c.env.transport; 23 | 24 | if (!transport) { 25 | throw new Error("Transport not initialized"); 26 | } 27 | 28 | await transport.handlePostMessage(c); 29 | return c.text("ok"); 30 | }); 31 | 32 | server.onError((err, c) => { 33 | console.error(err); 34 | return c.body(err.message, 500); 35 | }); 36 | 37 | export class MyDurableObject extends DurableObject { 38 | transport?: SSEHonoTransport; 39 | 40 | constructor(ctx: DurableObjectState, env: Env) { 41 | super(ctx, env); 42 | this.transport = new SSEHonoTransport("/messages", ctx.id.toString()); 43 | } 44 | 45 | async fetch(request: Request) { 46 | return server.fetch(request, { 47 | ...this.env, 48 | transport: this.transport, 49 | }); 50 | } 51 | } 52 | 53 | export default { 54 | async fetch( 55 | request: Request, 56 | env: { MY_DO: DurableObjectNamespace }, 57 | ctx: ExecutionContext, 58 | ): Promise { 59 | const url = new URL(request.url); 60 | const sessionId = url.searchParams.get("sessionId"); 61 | 62 | const namespace = env.MY_DO; 63 | 64 | let stub: DurableObjectStub; 65 | 66 | if (sessionId) stub = namespace.get(namespace.idFromString(sessionId)); 67 | else stub = namespace.get(namespace.newUniqueId()); 68 | 69 | return stub.fetch(request); 70 | }, 71 | }; 72 | ``` 73 | 74 | Over here we are creating a proxy server that will handle the SSE connection. The `MyDurableObject` class is a Durable Object that will handle the transport instance. The `fetch` method will handle the incoming requests and pass them to the Durable Object. 75 | 76 | To identify the session, we are using the `sessionId` query parameter. If it is not present, we will create a new Durable Object instance. If it is present, we will use the existing instance. 77 | -------------------------------------------------------------------------------- /docs/content/docs/runtimes/deno.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deno 3 | --- 4 | 5 | Here is an example of how to set up Muppet with Deno. You can use any transport layer you want, but here we will use the SSE transport layer. 6 | 7 | ```ts 8 | let transport: SSEHonoTransport | null = null; 9 | 10 | const server = new Hono(); 11 | 12 | server.get("/sse", (c) => { 13 | return streamSSE(c, async (stream) => { 14 | transport = new SSEHonoTransport("/messages"); 15 | transport.connectWithStream(stream); 16 | 17 | await bridge({ 18 | mcp, 19 | transport, 20 | }); 21 | }); 22 | }); 23 | 24 | server.post("/messages", async (c) => { 25 | if (!transport) { 26 | throw new Error("Transport not initialized"); 27 | } 28 | 29 | await transport.handlePostMessage(c); 30 | return c.text("ok"); 31 | }); 32 | 33 | server.onError((err, c) => { 34 | console.error(err); 35 | return c.body(err.message, 500); 36 | }); 37 | 38 | Deno.serve(server.fetch); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/content/docs/runtimes/nodejs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Node.js 3 | --- 4 | 5 | Here is an example of how to set up Muppet with Node.js. You can use any transport layer you want, but here we will use the SSE transport layer. 6 | 7 | ```ts 8 | let transport: SSEHonoTransport | null = null; 9 | 10 | const server = new Hono(); 11 | 12 | server.get("/sse", (c) => { 13 | return streamSSE(c, async (stream) => { 14 | transport = new SSEHonoTransport("/messages"); 15 | transport.connectWithStream(stream); 16 | 17 | await bridge({ 18 | mcp, 19 | transport, 20 | }); 21 | }); 22 | }); 23 | 24 | server.post("/messages", async (c) => { 25 | if (!transport) { 26 | throw new Error("Transport not initialized"); 27 | } 28 | 29 | await transport.handlePostMessage(c); 30 | return c.text("ok"); 31 | }); 32 | 33 | server.onError((err, c) => { 34 | console.error(err); 35 | return c.body(err.message, 500); 36 | }); 37 | 38 | serve({ 39 | fetch: server.fetch, 40 | port: 3000, 41 | }); 42 | ``` 43 | 44 | The `serve` function is from the `@hono/node-server` package. 45 | -------------------------------------------------------------------------------- /docs/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from '@/.source'; 2 | import { loader } from 'fumadocs-core/source'; 3 | 4 | // `loader()` also assign a URL to your pages 5 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info 6 | export const source = loader({ 7 | baseUrl: '/docs', 8 | source: docs.toFumadocsSource(), 9 | }); 10 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | redirects: async () => { 9 | return [ 10 | { 11 | source: "/", 12 | destination: "/docs", 13 | permanent: true, 14 | }, 15 | ]; 16 | }, 17 | }; 18 | 19 | export default withMDX(config); 20 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "start": "next start", 9 | "postinstall": "fumadocs-mdx" 10 | }, 11 | "dependencies": { 12 | "fast-glob": "^3.3.3", 13 | "fumadocs-core": "15.1.1", 14 | "fumadocs-mdx": "11.5.7", 15 | "fumadocs-ui": "15.1.1", 16 | "gray-matter": "^4.0.3", 17 | "next": "15.2.3", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-icons": "^5.5.0", 21 | "remark": "^15.0.1", 22 | "remark-gfm": "^4.0.1", 23 | "remark-mdx": "^3.1.0", 24 | "remark-stringify": "^11.0.0" 25 | }, 26 | "devDependencies": { 27 | "@hono/node-server": "^1.14.0", 28 | "@modelcontextprotocol/sdk": "^1.7.0", 29 | "@tailwindcss/postcss": "^4.0.14", 30 | "@types/express": "^5.0.1", 31 | "@types/mdx": "^2.0.13", 32 | "@types/node": "22.13.10", 33 | "@types/react": "^19.0.11", 34 | "@types/react-dom": "^19.0.4", 35 | "express": "^4.21.2", 36 | "hono": "^4.7.4", 37 | "hono-openapi": "^0.4.6", 38 | "muppet": "workspace:*", 39 | "postcss": "^8.5.3", 40 | "tailwindcss": "^4.0.14", 41 | "typescript": "^5.8.2", 42 | "zod": "^3.24.2" 43 | }, 44 | "nx": { 45 | "targets": { 46 | "build": { 47 | "dependsOn": [ 48 | "^build" 49 | ] 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /docs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muppet-dev/muppet/ceec9bdadfc7db258513615e5b56c87dd2aadf18/docs/public/banner.png -------------------------------------------------------------------------------- /docs/public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muppet-dev/muppet/ceec9bdadfc7db258513615e5b56c87dd2aadf18/docs/public/logo-dark.png -------------------------------------------------------------------------------- /docs/public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muppet-dev/muppet/ceec9bdadfc7db258513615e5b56c87dd2aadf18/docs/public/logo-light.png -------------------------------------------------------------------------------- /docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { defineDocs, defineConfig } from "fumadocs-mdx/config"; 2 | 3 | export const docs = defineDocs({ 4 | dir: "content/docs", 5 | }); 6 | 7 | export default defineConfig({ 8 | mdxOptions: {}, 9 | }); 10 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/.source": ["./.source/index.ts"], 20 | "@/*": ["./*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/bun-sse/README.md: -------------------------------------------------------------------------------- 1 | # Muppet SSE Example with Hono on Bun 2 | 3 | This example shows how to use the Muppet with SSE transport on Hono. We are using `SSEHonoTransport` transport layer from `muppet/streaming` to connect. 4 | 5 | This can be used with all the runtimes which supports Streaming with hono. More details about that here - 6 | -------------------------------------------------------------------------------- /examples/bun-sse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-sse", 3 | "version": "0.0.1", 4 | "bin": "./dist/index.js", 5 | "scripts": { 6 | "dev": "bun run --hot ./src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@hono/standard-validator": "^0.1.2", 10 | "@modelcontextprotocol/sdk": "^1.7.0", 11 | "hono": "^4.7.4", 12 | "muppet": "workspace:*", 13 | "zod": "^3.24.2" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^22.13.10", 17 | "typescript": "^5.8.2" 18 | }, 19 | "nx": { 20 | "targets": { 21 | "build": { 22 | "dependsOn": [ 23 | "^build" 24 | ] 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /examples/bun-sse/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { cors } from "hono/cors"; 3 | import { 4 | type ToolResponseType, 5 | bridge, 6 | describeTool, 7 | mValidator, 8 | muppet, 9 | } from "muppet"; 10 | import { SSEHonoTransport, streamSSE } from "muppet/streaming"; 11 | import z from "zod"; 12 | 13 | const app = new Hono(); 14 | 15 | app.post( 16 | "/hello", 17 | describeTool({ 18 | name: "greet-user-with-hello", 19 | description: 20 | "This will take in the name of the user and greet them. eg. Hello John", 21 | }), 22 | mValidator( 23 | "json", 24 | z.object({ 25 | name: z.string(), 26 | }), 27 | ), 28 | (c) => { 29 | const { name } = c.req.valid("json"); 30 | return c.json([ 31 | { 32 | type: "text", 33 | text: `Hello ${name}!`, 34 | }, 35 | ]); 36 | }, 37 | ); 38 | 39 | const mcp = muppet(app, { 40 | name: "My Muppet", 41 | version: "1.0.0", 42 | }); 43 | 44 | /** 45 | * For SSE transport 46 | */ 47 | let transport: SSEHonoTransport | null = null; 48 | 49 | const server = new Hono().use( 50 | cors({ 51 | origin: (origin) => origin, 52 | credentials: true, 53 | }), 54 | ); 55 | 56 | server.get("/sse", (c) => { 57 | return streamSSE(c, async (stream) => { 58 | transport = new SSEHonoTransport("/messages"); 59 | transport.connectWithStream(stream); 60 | 61 | await bridge({ 62 | mcp, 63 | transport, 64 | }); 65 | }); 66 | }); 67 | 68 | server.post("/messages", async (c) => { 69 | if (!transport) { 70 | throw new Error("Transport not initialized"); 71 | } 72 | 73 | await transport.handlePostMessage(c); 74 | return c.text("ok"); 75 | }); 76 | 77 | server.onError((err, c) => { 78 | console.error(err); 79 | return c.body(err.message, 500); 80 | }); 81 | 82 | export default server; 83 | -------------------------------------------------------------------------------- /examples/bun-sse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./src/**/*"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules/ 16 | .wrangler 17 | 18 | # env 19 | .env 20 | .env.production 21 | .dev.vars 22 | 23 | # logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | lerna-debug.log* 31 | 32 | # misc 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/README.md: -------------------------------------------------------------------------------- 1 | # Muppet SSE on the Edge 2 | 3 | This example shows how to use the Muppet SSE with Hono on the Edge. We are using `SSEServerTransport` transport layer from `muppet` to connect. 4 | -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-workers-sse", 3 | "scripts": { 4 | "dev": "wrangler dev", 5 | "deploy": "wrangler deploy --minify" 6 | }, 7 | "dependencies": { 8 | "@hono/standard-validator": "^0.1.2", 9 | "@modelcontextprotocol/sdk": "^1.8.0", 10 | "@valibot/to-json-schema": "^1.0.0", 11 | "effect": "^3.14.2", 12 | "hono": "4.7.4", 13 | "muppet": "workspace:*", 14 | "zod": "^3.24.2" 15 | }, 16 | "devDependencies": { 17 | "@cloudflare/workers-types": "^4.20250214.0", 18 | "wrangler": "^4.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | import { type Env, Hono } from "hono"; 3 | import { cors } from "hono/cors"; 4 | import { 5 | type ToolResponseType, 6 | bridge, 7 | describeTool, 8 | mValidator, 9 | muppet, 10 | } from "muppet"; 11 | import { SSEHonoTransport, streamSSE } from "muppet/streaming"; 12 | import z from "zod"; 13 | 14 | const app = new Hono(); 15 | 16 | app.post( 17 | "/hello", 18 | describeTool({ 19 | name: "greet-user-with-hello", 20 | description: 21 | "This will take in the name of the user and greet them. eg. Hello John", 22 | }), 23 | mValidator( 24 | "json", 25 | z.object({ 26 | name: z.string(), 27 | }), 28 | ), 29 | (c) => { 30 | const { name } = c.req.valid("json"); 31 | return c.json([ 32 | { 33 | type: "text", 34 | text: `Hello ${name}!`, 35 | }, 36 | ]); 37 | }, 38 | ); 39 | 40 | // Creating a mcp using muppet 41 | const mcp = muppet(app, { 42 | name: "My Muppet", 43 | version: "1.0.0", 44 | }); 45 | 46 | /** 47 | * For SSE transport 48 | */ 49 | const server = new Hono<{ Bindings: { transport: SSEHonoTransport } }>().use( 50 | cors({ 51 | origin: (origin) => origin, 52 | credentials: true, 53 | }), 54 | ); 55 | 56 | server.get("/sse", (c) => { 57 | return streamSSE(c, async (stream) => { 58 | c.env.transport.connectWithStream(stream); 59 | 60 | await bridge({ 61 | mcp, 62 | transport: c.env.transport, 63 | }); 64 | }); 65 | }); 66 | 67 | server.post("/messages", async (c) => { 68 | const transport = c.env.transport; 69 | 70 | if (!transport) { 71 | throw new Error("Transport not initialized"); 72 | } 73 | 74 | await transport.handlePostMessage(c); 75 | return c.text("ok"); 76 | }); 77 | 78 | server.onError((err, c) => { 79 | console.error(err); 80 | return c.body(err.message, 500); 81 | }); 82 | 83 | export class MyDurableObject extends DurableObject { 84 | transport?: SSEHonoTransport; 85 | 86 | constructor(ctx: DurableObjectState, env: Env) { 87 | super(ctx, env); 88 | this.transport = new SSEHonoTransport("/messages", ctx.id.toString()); 89 | } 90 | 91 | async fetch(request: Request) { 92 | return server.fetch(request, { 93 | ...this.env, 94 | transport: this.transport, 95 | }); 96 | } 97 | } 98 | 99 | export default { 100 | async fetch( 101 | request: Request, 102 | env: { MY_DO: DurableObjectNamespace }, 103 | ctx: ExecutionContext, 104 | ): Promise { 105 | const url = new URL(request.url); 106 | const sessionId = url.searchParams.get("sessionId"); 107 | 108 | const namespace = env.MY_DO; 109 | 110 | let stub: DurableObjectStub; 111 | 112 | if (sessionId) stub = namespace.get(namespace.idFromString(sessionId)); 113 | else stub = namespace.get(namespace.newUniqueId()); 114 | 115 | return stub.fetch(request); 116 | }, 117 | }; 118 | -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": [ 9 | "ESNext" 10 | ], 11 | "types": [ 12 | "@cloudflare/workers-types/2023-07-01" 13 | ], 14 | "jsx": "react-jsx", 15 | "jsxImportSource": "hono/jsx" 16 | }, 17 | } -------------------------------------------------------------------------------- /examples/cloudflare-workers-sse/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "muppet-sse", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-03-26", 6 | "durable_objects": { 7 | "bindings": [ 8 | { 9 | "name": "MY_DO", 10 | "class_name": "MyDurableObject" 11 | } 12 | ] 13 | }, 14 | "migrations": [{ "tag": "v1", "new_classes": ["MyDurableObject"] }] 15 | // "compatibility_flags": [ 16 | // "nodejs_compat" 17 | // ], 18 | // "vars": { 19 | // "MY_VAR": "my-variable" 20 | // }, 21 | // "kv_namespaces": [ 22 | // { 23 | // "binding": "MY_KV_NAMESPACE", 24 | // "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 25 | // } 26 | // ], 27 | // "r2_buckets": [ 28 | // { 29 | // "binding": "MY_BUCKET", 30 | // "bucket_name": "my-bucket" 31 | // } 32 | // ], 33 | // "d1_databases": [ 34 | // { 35 | // "binding": "MY_DB", 36 | // "database_name": "my-database", 37 | // "database_id": "" 38 | // } 39 | // ], 40 | // "ai": { 41 | // "binding": "AI" 42 | // }, 43 | // "observability": { 44 | // "enabled": true, 45 | // "head_sampling_rate": 1 46 | // } 47 | } 48 | -------------------------------------------------------------------------------- /examples/deno-sse/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@hono/standard-validator": "npm:@hono/standard-validator@^0.1.2", 4 | "hono": "npm:hono@^4.7.5", 5 | "muppet": "npm:muppet@latest", 6 | "zod": "npm:zod@3.24.2" 7 | }, 8 | "tasks": { 9 | "start": "deno run --allow-net main.ts" 10 | }, 11 | "compilerOptions": { 12 | "jsx": "precompile", 13 | "jsxImportSource": "hono/jsx" 14 | } 15 | } -------------------------------------------------------------------------------- /examples/deno-sse/main.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | type ToolResponseType, 4 | bridge, 5 | describeTool, 6 | mValidator, 7 | muppet, 8 | } from "muppet"; 9 | import { SSEHonoTransport, streamSSE } from "muppet/streaming"; 10 | import z from "zod"; 11 | 12 | const app = new Hono(); 13 | 14 | app.post( 15 | "/hello", 16 | describeTool({ 17 | name: "greet-user-with-hello", 18 | description: 19 | "This will take in the name of the user and greet them. eg. Hello John", 20 | }), 21 | mValidator( 22 | "json", 23 | z.object({ 24 | name: z.string(), 25 | }), 26 | ), 27 | (c) => { 28 | const { name } = c.req.valid("json"); 29 | return c.json([ 30 | { 31 | type: "text", 32 | text: `Hello ${name}!`, 33 | }, 34 | ]); 35 | }, 36 | ); 37 | 38 | const mcp = muppet(app, { 39 | name: "My Muppet", 40 | version: "1.0.0", 41 | }); 42 | 43 | /** 44 | * For SSE transport 45 | */ 46 | let transport: SSEHonoTransport | null = null; 47 | 48 | const server = new Hono().use( 49 | cors({ 50 | origin: (origin) => origin, 51 | credentials: true, 52 | }), 53 | ); 54 | 55 | server.get("/sse", (c) => { 56 | return streamSSE(c, async (stream) => { 57 | transport = new SSEHonoTransport("/messages"); 58 | transport.connectWithStream(stream); 59 | 60 | await bridge({ 61 | mcp, 62 | transport, 63 | }); 64 | }); 65 | }); 66 | 67 | server.post("/messages", async (c) => { 68 | if (!transport) { 69 | throw new Error("Transport not initialized"); 70 | } 71 | 72 | await transport.handlePostMessage(c); 73 | return c.text("ok"); 74 | }); 75 | 76 | server.onError((err, c) => { 77 | console.error(err); 78 | return c.body(err.message, 500); 79 | }); 80 | 81 | Deno.serve(server.fetch); 82 | 83 | function cors(arg0: { 84 | origin: (origin: any) => any; 85 | credentials: boolean; 86 | }): any { 87 | throw new Error("Function not implemented."); 88 | } 89 | -------------------------------------------------------------------------------- /examples/deno-stdio/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.7.0", 4 | "@hono/standard-validator": "npm:@hono/standard-validator@^0.1.2", 5 | "hono": "npm:hono@^4.7.5", 6 | "muppet": "npm:muppet@latest", 7 | "zod": "npm:zod@3.24.2" 8 | }, 9 | "tasks": { 10 | "start": "deno run --allow-net main.ts" 11 | }, 12 | "compilerOptions": { 13 | "jsx": "precompile", 14 | "jsxImportSource": "hono/jsx" 15 | } 16 | } -------------------------------------------------------------------------------- /examples/deno-stdio/main.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { Hono } from "hono"; 3 | import { 4 | type ToolResponseType, 5 | bridge, 6 | describeTool, 7 | mValidator, 8 | muppet, 9 | } from "muppet"; 10 | import z from "zod"; 11 | 12 | const app = new Hono(); 13 | 14 | app.post( 15 | "/hello", 16 | describeTool({ 17 | name: "greet-user-with-hello", 18 | description: 19 | "This will take in the name of the user and greet them. eg. Hello John", 20 | }), 21 | mValidator( 22 | "json", 23 | z.object({ 24 | name: z.string(), 25 | }), 26 | ), 27 | (c) => { 28 | const { name } = c.req.valid("json"); 29 | return c.json([ 30 | { 31 | type: "text", 32 | text: `Hello ${name}!`, 33 | }, 34 | ]); 35 | }, 36 | ); 37 | 38 | // Creating a mcp using muppet 39 | const mcp = muppet(app, { 40 | name: "My Muppet", 41 | version: "1.0.0", 42 | }); 43 | 44 | bridge({ 45 | mcp, 46 | transport: new StdioServerTransport(), 47 | }); 48 | -------------------------------------------------------------------------------- /examples/example-forecast/README.md: -------------------------------------------------------------------------------- 1 | # Example - Forecast MCP 2 | 3 | This example shows how to build a simple weather server using Muppet and Hono. The server exposes two tools: `get-alerts` and `get-forecast`, which can be used to fetch weather alerts and forecasts, respectively. 4 | 5 | You can learn more about this example here - 6 | -------------------------------------------------------------------------------- /examples/example-forecast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-forecast", 3 | "version": "0.0.1", 4 | "bin": "./dist/index.js", 5 | "scripts": { 6 | "dev": "tsx --watch ./src/index.ts", 7 | "build": "pkgroll --minify" 8 | }, 9 | "dependencies": { 10 | "@hono/node-server": "^1.14.0", 11 | "@hono/standard-validator": "^0.1.2", 12 | "@modelcontextprotocol/sdk": "^1.7.0", 13 | "hono": "^4.7.4", 14 | "muppet": "workspace:*", 15 | "zod": "^3.24.2" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.13.10", 19 | "pkgroll": "^2.11.2", 20 | "tsx": "^4.19.3", 21 | "typescript": "^5.8.2" 22 | }, 23 | "nx": { 24 | "targets": { 25 | "build": { 26 | "dependsOn": [ 27 | "^build" 28 | ] 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /examples/example-forecast/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | type ToolResponseType, 4 | describeTool, 5 | mValidator, 6 | muppet, 7 | bridge, 8 | } from "muppet"; 9 | import z from "zod"; 10 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 11 | 12 | const NWS_API_BASE = "https://api.weather.gov"; 13 | const USER_AGENT = "weather-app/1.0"; 14 | 15 | // Helper function for making NWS API requests 16 | async function makeNWSRequest(url: string): Promise { 17 | const headers = { 18 | "User-Agent": USER_AGENT, 19 | Accept: "application/geo+json", 20 | }; 21 | 22 | try { 23 | const response = await fetch(url, { headers }); 24 | if (!response.ok) { 25 | throw new Error(`HTTP error! status: ${response.status}`); 26 | } 27 | return (await response.json()) as T; 28 | } catch (error) { 29 | console.error("Error making NWS request:", error); 30 | return null; 31 | } 32 | } 33 | 34 | interface AlertFeature { 35 | properties: { 36 | event?: string; 37 | areaDesc?: string; 38 | severity?: string; 39 | status?: string; 40 | headline?: string; 41 | }; 42 | } 43 | 44 | // Format alert data 45 | function formatAlert(feature: AlertFeature): string { 46 | const props = feature.properties; 47 | return [ 48 | `Event: ${props.event || "Unknown"}`, 49 | `Area: ${props.areaDesc || "Unknown"}`, 50 | `Severity: ${props.severity || "Unknown"}`, 51 | `Status: ${props.status || "Unknown"}`, 52 | `Headline: ${props.headline || "No headline"}`, 53 | "---", 54 | ].join("\n"); 55 | } 56 | 57 | interface ForecastPeriod { 58 | name?: string; 59 | temperature?: number; 60 | temperatureUnit?: string; 61 | windSpeed?: string; 62 | windDirection?: string; 63 | shortForecast?: string; 64 | } 65 | 66 | interface AlertsResponse { 67 | features: AlertFeature[]; 68 | } 69 | 70 | interface PointsResponse { 71 | properties: { 72 | forecast?: string; 73 | }; 74 | } 75 | 76 | interface ForecastResponse { 77 | properties: { 78 | periods: ForecastPeriod[]; 79 | }; 80 | } 81 | 82 | const app = new Hono(); 83 | 84 | // Define the get-alerts tool 85 | app.post( 86 | "/get-alerts", 87 | describeTool({ 88 | name: "get-alerts", 89 | description: "Get weather alerts for a state", 90 | }), 91 | mValidator( 92 | "json", 93 | z.object({ 94 | state: z 95 | .string() 96 | .length(2) 97 | .describe("Two-letter state code (e.g. CA, NY)"), 98 | }), 99 | ), 100 | async (c) => { 101 | const { state } = c.req.valid("json"); 102 | 103 | const stateCode = state.toUpperCase(); 104 | const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; 105 | const alertsData = await makeNWSRequest(alertsUrl); 106 | 107 | if (!alertsData) { 108 | return c.json({ 109 | content: [ 110 | { 111 | type: "text", 112 | text: "Failed to retrieve alerts data", 113 | }, 114 | ], 115 | }); 116 | } 117 | 118 | const features = alertsData.features || []; 119 | if (features.length === 0) { 120 | return c.json({ 121 | content: [ 122 | { 123 | type: "text", 124 | text: `No active alerts for ${stateCode}`, 125 | }, 126 | ], 127 | }); 128 | } 129 | 130 | const formattedAlerts = features.map(formatAlert); 131 | const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`; 132 | 133 | return c.json({ 134 | content: [ 135 | { 136 | type: "text", 137 | text: alertsText, 138 | }, 139 | ], 140 | }); 141 | }, 142 | ); 143 | 144 | app.post( 145 | "/get-forecast", 146 | describeTool({ 147 | name: "get-forecast", 148 | description: "Get weather forecast for a location", 149 | }), 150 | mValidator( 151 | "json", 152 | z.object({ 153 | latitude: z 154 | .number() 155 | .min(-90) 156 | .max(90) 157 | .describe("Latitude of the location"), 158 | longitude: z 159 | .number() 160 | .min(-180) 161 | .max(180) 162 | .describe("Longitude of the location"), 163 | }), 164 | ), 165 | async (c) => { 166 | const { latitude, longitude } = c.req.valid("json"); 167 | 168 | // Get grid point data 169 | const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; 170 | const pointsData = await makeNWSRequest(pointsUrl); 171 | 172 | if (!pointsData) { 173 | return c.json({ 174 | content: [ 175 | { 176 | type: "text", 177 | text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, 178 | }, 179 | ], 180 | }); 181 | } 182 | 183 | const forecastUrl = pointsData.properties?.forecast; 184 | if (!forecastUrl) { 185 | return c.json({ 186 | content: [ 187 | { 188 | type: "text", 189 | text: "Failed to get forecast URL from grid point data", 190 | }, 191 | ], 192 | }); 193 | } 194 | 195 | // Get forecast data 196 | const forecastData = await makeNWSRequest(forecastUrl); 197 | if (!forecastData) { 198 | return c.json({ 199 | content: [ 200 | { 201 | type: "text", 202 | text: "Failed to retrieve forecast data", 203 | }, 204 | ], 205 | }); 206 | } 207 | 208 | const periods = forecastData.properties?.periods || []; 209 | if (periods.length === 0) { 210 | return c.json({ 211 | content: [ 212 | { 213 | type: "text", 214 | text: "No forecast periods available", 215 | }, 216 | ], 217 | }); 218 | } 219 | 220 | // Format forecast periods 221 | const formattedForecast = periods.map((period: ForecastPeriod) => 222 | [ 223 | `${period.name || "Unknown"}:`, 224 | `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`, 225 | `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`, 226 | `${period.shortForecast || "No forecast available"}`, 227 | "---", 228 | ].join("\n"), 229 | ); 230 | 231 | const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`; 232 | 233 | return c.json({ 234 | content: [ 235 | { 236 | type: "text", 237 | text: forecastText, 238 | }, 239 | ], 240 | }); 241 | }, 242 | ); 243 | 244 | const mcp = muppet(app, { 245 | name: "weather", 246 | version: "1.0.0", 247 | }); 248 | 249 | // Bridge the mcp with the transport 250 | bridge({ 251 | // Creating a mcp using muppet 252 | mcp, 253 | transport: new StdioServerTransport(), 254 | }); 255 | -------------------------------------------------------------------------------- /examples/example-forecast/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./src/**/*"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/nodejs-sse/README.md: -------------------------------------------------------------------------------- 1 | # Muppet SSE Example with Hono 2 | 3 | This example shows how to use the Muppet with SSE transport on Hono. We are using `SSEHonoTransport` transport layer from `muppet/streaming` to connect. 4 | 5 | This can be used with all the runtimes which supports Streaming with hono. More details about that here - 6 | -------------------------------------------------------------------------------- /examples/nodejs-sse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-sse", 3 | "version": "0.0.1", 4 | "bin": "./dist/index.js", 5 | "scripts": { 6 | "dev": "tsx --watch ./src/index.ts", 7 | "build": "pkgroll --minify" 8 | }, 9 | "dependencies": { 10 | "@hono/node-server": "^1.14.0", 11 | "@hono/standard-validator": "^0.1.2", 12 | "@modelcontextprotocol/sdk": "^1.7.0", 13 | "hono": "^4.7.4", 14 | "muppet": "workspace:*", 15 | "zod": "^3.24.2" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.13.10", 19 | "pkgroll": "^2.11.2", 20 | "tsx": "^4.19.3", 21 | "typescript": "^5.8.2" 22 | }, 23 | "nx": { 24 | "targets": { 25 | "build": { 26 | "dependsOn": [ 27 | "^build" 28 | ] 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /examples/nodejs-sse/src/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { Hono } from "hono"; 3 | import { cors } from "hono/cors"; 4 | import { 5 | type ToolResponseType, 6 | bridge, 7 | describeTool, 8 | mValidator, 9 | muppet, 10 | } from "muppet"; 11 | import { SSEHonoTransport, streamSSE } from "muppet/streaming"; 12 | import z from "zod"; 13 | 14 | const app = new Hono(); 15 | 16 | app.post( 17 | "/hello", 18 | describeTool({ 19 | name: "greet-user-with-hello", 20 | description: 21 | "This will take in the name of the user and greet them. eg. Hello John", 22 | }), 23 | mValidator( 24 | "json", 25 | z.object({ 26 | name: z.string(), 27 | }), 28 | ), 29 | (c) => { 30 | const { name } = c.req.valid("json"); 31 | return c.json([ 32 | { 33 | type: "text", 34 | text: `Hello ${name}!`, 35 | }, 36 | ]); 37 | }, 38 | ); 39 | 40 | const mcp = muppet(app, { 41 | name: "My Muppet", 42 | version: "1.0.0", 43 | }); 44 | 45 | /** 46 | * For SSE transport 47 | */ 48 | let transport: SSEHonoTransport | null = null; 49 | 50 | const server = new Hono().use( 51 | cors({ 52 | origin: (origin) => origin, 53 | credentials: true, 54 | }), 55 | ); 56 | 57 | server.get("/sse", (c) => { 58 | return streamSSE(c, async (stream) => { 59 | transport = new SSEHonoTransport("/messages"); 60 | transport.connectWithStream(stream); 61 | 62 | await bridge({ 63 | mcp, 64 | transport, 65 | }); 66 | }); 67 | }); 68 | 69 | server.post("/messages", async (c) => { 70 | if (!transport) { 71 | throw new Error("Transport not initialized"); 72 | } 73 | 74 | await transport.handlePostMessage(c); 75 | return c.text("ok"); 76 | }); 77 | 78 | server.onError((err, c) => { 79 | console.error(err); 80 | return c.body(err.message, 500); 81 | }); 82 | 83 | serve({ 84 | fetch: server.fetch, 85 | port: 3000, 86 | }); 87 | -------------------------------------------------------------------------------- /examples/nodejs-sse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./src/**/*"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-sse-express/README.md: -------------------------------------------------------------------------------- 1 | # Muppet SSE Example with Express 2 | 3 | This example shows how to use the Muppet with SSE transport on Hono and Express. We are using `SSEServerTransport` transport layer from `@modelcontextprotocol/sdk` to connect. 4 | 5 | The `SSEServerTransport` class uses some node specific modules, so it can only be used in a node environment. 6 | -------------------------------------------------------------------------------- /examples/with-sse-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-sse-express", 3 | "version": "0.0.1", 4 | "bin": "./dist/index.js", 5 | "scripts": { 6 | "dev": "tsx --watch ./src/index.ts", 7 | "build": "pkgroll --minify" 8 | }, 9 | "dependencies": { 10 | "@modelcontextprotocol/sdk": "^1.7.0", 11 | "express": "^4.21.2", 12 | "hono": "^4.7.4", 13 | "muppet": "workspace:*", 14 | "zod": "^3.24.2" 15 | }, 16 | "devDependencies": { 17 | "@types/express": "^5.0.1", 18 | "@types/node": "^22.13.10", 19 | "pkgroll": "^2.11.2", 20 | "tsx": "^4.19.3", 21 | "typescript": "^5.8.2" 22 | }, 23 | "nx": { 24 | "targets": { 25 | "build": { 26 | "dependsOn": ["^build"] 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-sse-express/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 2 | import express from "express"; 3 | import { Hono } from "hono"; 4 | import { 5 | type ToolResponseType, 6 | bridge, 7 | describeTool, 8 | mValidator, 9 | muppet, 10 | } from "muppet"; 11 | import z from "zod"; 12 | 13 | const app = new Hono(); 14 | 15 | /** 16 | * This is a simple 'hello world', which takes a name as input and returns a greeting 17 | */ 18 | app.post( 19 | "/hello", 20 | describeTool({ 21 | name: "greet-user-with-hello", 22 | description: 23 | "This will take in the name of the user and greet them. eg. Hello John", 24 | }), 25 | mValidator( 26 | "json", 27 | z.object({ 28 | name: z.string().optional(), 29 | }), 30 | ), 31 | (c) => { 32 | const payload = c.req.valid("json"); 33 | return c.json([ 34 | { 35 | type: "text", 36 | text: `Hello ${payload.name ?? "World"}!`, 37 | }, 38 | ]); 39 | }, 40 | ); 41 | 42 | // Creating a mcp using muppet 43 | const mcp = muppet(app, { 44 | name: "My Muppet", 45 | version: "1.0.0", 46 | }); 47 | 48 | let transport: SSEServerTransport | null = null; 49 | 50 | const server = express(); 51 | 52 | server.get("/sse", async (_, res) => { 53 | // Initialize the transport 54 | transport = new SSEServerTransport("/messages", res); 55 | 56 | // Bridge the mcp with the transport 57 | bridge({ 58 | mcp, 59 | transport, 60 | }); 61 | }); 62 | 63 | /** 64 | * This is the endpoint where the client will send the messages 65 | */ 66 | server.post("/messages", (req, res) => { 67 | if (transport) { 68 | transport.handlePostMessage(req, res); 69 | } 70 | }); 71 | 72 | const PORT = 3001; 73 | 74 | server.listen(PORT, () => { 75 | console.log(`Server started on port ${PORT}`); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/with-sse-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./src/**/*"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-stdio/README.md: -------------------------------------------------------------------------------- 1 | # Muppet Stdio Example 2 | 3 | This example shows how to use the Muppet with Stdio transport on Hono. We are using `StdioServerTransport` transport layer from `@modelcontextprotocol/sdk` to connect. 4 | -------------------------------------------------------------------------------- /examples/with-stdio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-stdio", 3 | "version": "0.0.1", 4 | "bin": "./dist/index.js", 5 | "scripts": { 6 | "dev": "tsx --watch ./src/index.ts", 7 | "build": "pkgroll --minify" 8 | }, 9 | "dependencies": { 10 | "@modelcontextprotocol/sdk": "^1.7.0", 11 | "hono": "4.7.4", 12 | "muppet": "workspace:*", 13 | "zod": "^3.24.2" 14 | }, 15 | "devDependencies": { 16 | "pkgroll": "^2.11.2", 17 | "tsx": "^4.19.3", 18 | "typescript": "^5.8.2" 19 | }, 20 | "nx": { 21 | "targets": { 22 | "build": { 23 | "dependsOn": [ 24 | "^build" 25 | ] 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /examples/with-stdio/src/index.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { Hono } from "hono"; 3 | import { 4 | type ToolResponseType, 5 | bridge, 6 | describeTool, 7 | mValidator, 8 | muppet, 9 | } from "muppet"; 10 | import z from "zod"; 11 | 12 | const app = new Hono(); 13 | 14 | app.post( 15 | "/hello", 16 | describeTool({ 17 | name: "greet-user-with-hello", 18 | description: 19 | "This will take in the name of the user and greet them. eg. Hello John", 20 | }), 21 | mValidator( 22 | "json", 23 | z.object({ 24 | name: z.string(), 25 | }), 26 | ), 27 | (c) => { 28 | const { name } = c.req.valid("json"); 29 | return c.json([ 30 | { 31 | type: "text", 32 | text: `Hello ${name}!`, 33 | }, 34 | ]); 35 | }, 36 | ); 37 | 38 | // Creating a mcp using muppet 39 | const mcp = muppet(app, { 40 | name: "My Muppet", 41 | version: "1.0.0", 42 | }); 43 | 44 | bridge({ 45 | mcp, 46 | transport: new StdioServerTransport(), 47 | }); 48 | -------------------------------------------------------------------------------- /examples/with-stdio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./src/**/*"] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@muppet/workspace", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "commit": "czg", 6 | "prepare": "is-ci || husky", 7 | "format": "biome check --write ." 8 | }, 9 | "devDependencies": { 10 | "@biomejs/biome": "^1.9.4", 11 | "czg": "^1.11.1", 12 | "husky": "^9.1.7", 13 | "is-ci": "^4.1.0", 14 | "nano-staged": "^0.8.0", 15 | "nx": "^20.6.2", 16 | "typescript": "^5.8.2", 17 | "vitest": "^3.0.9" 18 | }, 19 | "packageManager": "pnpm@8.14.0+sha256.9cebf61abd83f68177b29484da72da9751390eaad46dfc3072d266bfbb1ba7bf" 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Muppet 2 | 3 | [![npm version](https://img.shields.io/npm/v/muppet.svg)](https://npmjs.org/package/muppet "View this project on NPM") 4 | [![npm downloads](https://img.shields.io/npm/dm/muppet)](https://www.npmjs.com/package/muppet) 5 | [![license](https://img.shields.io/npm/l/muppet)](LICENSE) 6 | 7 | Muppet is an open-source toolkit designed to simplify the entire lifecycle of your Model Context Protocol (MCP) servers—from development and testing to deployment. 8 | 9 | For documentation visit [muppet.dev](https://muppet.dev). 10 | 11 | ## Contributing 12 | 13 | Visit our [contributing docs](https://github.com/muppet-dev/muppet/blob/main/CONTRIBUTING.md). 14 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muppet", 3 | "description": "Toolkit for building MCPs on Honojs", 4 | "version": "0.2.2", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "dist/index.cjs", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "dev": "pkgroll --watch", 15 | "build": "pkgroll --minify --clean-dist", 16 | "test": "vitest" 17 | }, 18 | "keywords": [ 19 | "hono", 20 | "mcps", 21 | "mcp", 22 | "standard-schema", 23 | "toolkit" 24 | ], 25 | "homepage": "https://github.com/muppet-dev/muppet", 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/muppet-dev/muppet.git", 32 | "directory": "packages/core" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/muppet-dev/muppet/issues" 36 | }, 37 | "peerDependencies": { 38 | "@hono/standard-validator": "^0.1.2", 39 | "@modelcontextprotocol/sdk": "^1.7.0", 40 | "@standard-community/standard-json": "^0.1.2", 41 | "@standard-schema/spec": "^1.0.0", 42 | "hono": "^4.6.13", 43 | "openapi-types": "^12.1.3" 44 | }, 45 | "peerDependenciesMeta": { 46 | "@standard-schema/spec": { 47 | "optional": true 48 | }, 49 | "hono": { 50 | "optional": true 51 | }, 52 | "openapi-types": { 53 | "optional": true 54 | } 55 | }, 56 | "exports": { 57 | ".": { 58 | "import": { 59 | "types": "./dist/index.d.ts", 60 | "default": "./dist/index.js" 61 | }, 62 | "require": { 63 | "types": "./dist/index.d.cts", 64 | "default": "./dist/index.cjs" 65 | } 66 | }, 67 | "./streaming": { 68 | "import": { 69 | "types": "./dist/streaming.d.ts", 70 | "default": "./dist/streaming.js" 71 | }, 72 | "require": { 73 | "types": "./dist/streaming.d.cts", 74 | "default": "./dist/streaming.cjs" 75 | } 76 | }, 77 | "./openapi": { 78 | "import": { 79 | "types": "./dist/openapi.d.ts", 80 | "default": "./dist/openapi.js" 81 | }, 82 | "require": { 83 | "types": "./dist/openapi.d.cts", 84 | "default": "./dist/openapi.cjs" 85 | } 86 | } 87 | }, 88 | "devDependencies": { 89 | "@hono/event-emitter": "^2.0.0", 90 | "@types/json-schema": "^7.0.15", 91 | "pino": "^9.6.0", 92 | "pkgroll": "^2.5.1", 93 | "zod": "^3.24.2" 94 | } 95 | } -------------------------------------------------------------------------------- /packages/core/src/__tests__/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { Hono } from "hono"; 3 | import z from "zod"; 4 | import { 5 | describeTool, 6 | describePrompt, 7 | mValidator, 8 | type ToolResponseType, 9 | registerResources, 10 | type PromptResponseType, 11 | muppet, 12 | } from "../index"; 13 | 14 | describe("basic", async () => { 15 | const app = new Hono() 16 | .post( 17 | "/hello", 18 | describeTool({ 19 | name: "Hello World", 20 | description: "A simple hello world route", 21 | }), 22 | mValidator( 23 | "json", 24 | z.object({ 25 | name: z.string(), 26 | }), 27 | ), 28 | (c) => { 29 | const payload = c.req.valid("json"); 30 | return c.json([ 31 | { 32 | type: "text", 33 | text: `Hello ${payload.name}!`, 34 | }, 35 | ]); 36 | }, 37 | ) 38 | .post( 39 | "/documents", 40 | registerResources((c) => { 41 | return [ 42 | { 43 | uri: "https://lorem.ipsum", 44 | name: "Todo list", 45 | mimeType: "text/plain", 46 | }, 47 | { 48 | type: "template", 49 | uri: "https://lorem.{ending}", 50 | name: "Todo list", 51 | mimeType: "text/plain", 52 | }, 53 | ]; 54 | }), 55 | ) 56 | .post( 57 | "/simple", 58 | describePrompt({ name: "Simple Prompt" }), 59 | mValidator( 60 | "json", 61 | z.object({ 62 | name: z.string(), 63 | }), 64 | ), 65 | (c) => { 66 | const { name } = c.req.valid("json"); 67 | return c.json([ 68 | { 69 | role: "user", 70 | content: { 71 | type: "text", 72 | text: `This is a simple prompt for ${name}`, 73 | }, 74 | }, 75 | ]); 76 | }, 77 | ); 78 | 79 | const serverInfo = { 80 | name: "My Muppet", 81 | version: "1.0.0", 82 | }; 83 | 84 | // Creating a mcp using muppet 85 | const instance = await muppet(app, { 86 | ...serverInfo, 87 | resources: { 88 | https: (uri) => { 89 | if (uri === "https://lorem.ipsum") 90 | return [ 91 | { 92 | uri: "task1", 93 | text: "This is a fixed task", 94 | }, 95 | ]; 96 | 97 | return [ 98 | { 99 | uri: "task1", 100 | text: "This is dynamic task", 101 | }, 102 | { 103 | uri: "task2", 104 | text: "Could be fetched from a DB", 105 | }, 106 | ]; 107 | }, 108 | }, 109 | }); 110 | 111 | it("should initialize", async () => { 112 | const response = await instance?.request("/initialize", { 113 | method: "POST", 114 | body: JSON.stringify({ 115 | jsonrpc: "2.0", 116 | id: 0, 117 | method: "initialize", 118 | params: { 119 | protocolVersion: "2024-11-05", 120 | capabilities: { sampling: {}, roots: { listChanged: true } }, 121 | clientInfo: { name: "mcp-inspector", version: "0.7.0" }, 122 | }, 123 | }), 124 | headers: { 125 | "content-type": "application/json", 126 | }, 127 | }); 128 | 129 | const json = await response?.json(); 130 | 131 | expect(json).toBeDefined(); 132 | expect(json.result).toMatchObject({ 133 | protocolVersion: "2024-11-05", 134 | serverInfo, 135 | capabilities: { tools: {}, prompts: {}, resources: {} }, 136 | }); 137 | }); 138 | 139 | it("should list tools", async () => { 140 | const response = await instance?.request("/tools/list", { 141 | method: "POST", 142 | body: JSON.stringify({ 143 | jsonrpc: "2.0", 144 | id: 1, 145 | method: "tools/list", 146 | params: {}, 147 | }), 148 | headers: { 149 | "content-type": "application/json", 150 | }, 151 | }); 152 | 153 | const json = await response?.json(); 154 | 155 | expect(json).toBeDefined(); 156 | expect(json.result).toMatchObject({ 157 | tools: [ 158 | { 159 | name: "Hello World", 160 | description: "A simple hello world route", 161 | inputSchema: { 162 | type: "object", 163 | properties: { name: { type: "string" } }, 164 | required: ["name"], 165 | additionalProperties: false, 166 | $schema: "http://json-schema.org/draft-07/schema#", 167 | }, 168 | }, 169 | ], 170 | }); 171 | }); 172 | 173 | it("should call tool", async () => { 174 | const response = await instance?.request("/tools/call", { 175 | method: "POST", 176 | body: JSON.stringify({ 177 | jsonrpc: "2.0", 178 | id: 2, 179 | method: "tools/call", 180 | params: { 181 | _meta: { progressToken: 0 }, 182 | name: "Hello World", 183 | arguments: { name: "muppet" }, 184 | }, 185 | }), 186 | headers: { 187 | "content-type": "application/json", 188 | }, 189 | }); 190 | 191 | const json = await response?.json(); 192 | 193 | expect(json).toBeDefined(); 194 | expect(json.result).toMatchObject({ 195 | content: [ 196 | { 197 | type: "text", 198 | text: "Hello muppet!", 199 | }, 200 | ], 201 | }); 202 | }); 203 | 204 | it("should list prompts", async () => { 205 | const response = await instance?.request("/prompts/list", { 206 | method: "POST", 207 | body: JSON.stringify({ 208 | jsonrpc: "2.0", 209 | id: 1, 210 | method: "prompts/list", 211 | params: {}, 212 | }), 213 | headers: { 214 | "content-type": "application/json", 215 | }, 216 | }); 217 | 218 | const json = await response?.json(); 219 | 220 | expect(json).toBeDefined(); 221 | expect(json.result).toMatchObject({ 222 | prompts: [ 223 | { 224 | name: "Simple Prompt", 225 | arguments: [{ name: "name", required: true }], 226 | method: "POST", 227 | schema: { 228 | json: { 229 | type: "object", 230 | properties: { name: { type: "string" } }, 231 | required: ["name"], 232 | additionalProperties: false, 233 | $schema: "http://json-schema.org/draft-07/schema#", 234 | }, 235 | }, 236 | }, 237 | ], 238 | }); 239 | }); 240 | 241 | it("should get prompt", async () => { 242 | const response = await instance?.request("/prompts/get", { 243 | method: "POST", 244 | body: JSON.stringify({ 245 | jsonrpc: "2.0", 246 | id: 3, 247 | method: "prompts/get", 248 | params: { name: "Simple Prompt", arguments: { name: "Muppet" } }, 249 | }), 250 | headers: { 251 | "content-type": "application/json", 252 | }, 253 | }); 254 | 255 | const json = await response?.json(); 256 | 257 | expect(json).toBeDefined(); 258 | expect(json.result).toMatchObject({ 259 | messages: [ 260 | { 261 | role: "user", 262 | content: { type: "text", text: "This is a simple prompt for Muppet" }, 263 | }, 264 | ], 265 | }); 266 | }); 267 | 268 | it("should list resources", async () => { 269 | const response = await instance?.request("/resources/list", { 270 | method: "POST", 271 | body: JSON.stringify({ 272 | jsonrpc: "2.0", 273 | id: 4, 274 | method: "resources/list", 275 | params: {}, 276 | }), 277 | headers: { 278 | "content-type": "application/json", 279 | }, 280 | }); 281 | 282 | const json = await response?.json(); 283 | 284 | expect(json).toBeDefined(); 285 | expect(json.result).toMatchObject({ 286 | resources: [ 287 | { 288 | uri: "https://lorem.ipsum", 289 | name: "Todo list", 290 | mimeType: "text/plain", 291 | }, 292 | ], 293 | }); 294 | }); 295 | 296 | it("should list template resources", async () => { 297 | const response = await instance?.request("/resources/templates/list", { 298 | method: "POST", 299 | body: JSON.stringify({ 300 | jsonrpc: "2.0", 301 | id: 6, 302 | method: "resources/templates/list", 303 | params: {}, 304 | }), 305 | headers: { 306 | "content-type": "application/json", 307 | }, 308 | }); 309 | 310 | const json = await response?.json(); 311 | 312 | expect(json).toBeDefined(); 313 | expect(json.result).toMatchObject({ 314 | resourceTemplates: [ 315 | { 316 | uriTemplate: "https://lorem.{ending}", 317 | name: "Todo list", 318 | mimeType: "text/plain", 319 | }, 320 | ], 321 | }); 322 | }); 323 | 324 | it("should read the resource", async () => { 325 | const response = await instance?.request("/resources/read", { 326 | method: "POST", 327 | body: JSON.stringify({ 328 | jsonrpc: "2.0", 329 | id: 5, 330 | method: "resources/read", 331 | params: { uri: "https://lorem.ipsum" }, 332 | }), 333 | headers: { 334 | "content-type": "application/json", 335 | }, 336 | }); 337 | 338 | const json = await response?.json(); 339 | 340 | expect(json).toBeDefined(); 341 | expect(json.result).toMatchObject({ 342 | contents: [{ uri: "task1", text: "This is a fixed task" }], 343 | }); 344 | }); 345 | 346 | it("should respond with completions", async () => { 347 | const response = await instance?.request("/completion/complete", { 348 | method: "POST", 349 | body: JSON.stringify({ 350 | jsonrpc: "2.0", 351 | id: 2, 352 | method: "completion/complete", 353 | params: { 354 | argument: { name: "name", value: "Aditya" }, 355 | ref: { type: "ref/prompt", name: "Simple Prompt" }, 356 | }, 357 | }), 358 | headers: { 359 | "content-type": "application/json", 360 | }, 361 | }); 362 | 363 | const json = await response?.json(); 364 | 365 | expect(json).toBeDefined(); 366 | expect(json.result).toMatchObject({ 367 | completion: { values: [], total: 0, hasMore: false }, 368 | }); 369 | }); 370 | }); 371 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/openapi.test.ts: -------------------------------------------------------------------------------- 1 | import { fromOpenAPI } from "../openapi"; 2 | import { describe, it, expect } from "vitest"; 3 | 4 | describe("instance from OpenAPI", async () => { 5 | const instance = fromOpenAPI({ 6 | openapi: "3.1.0", 7 | info: { 8 | title: "Muppet", 9 | description: "API for greeting an creating users", 10 | version: "1.0.0", 11 | }, 12 | servers: [{ url: "http://localhost:3000", description: "Local server" }], 13 | paths: { 14 | "/zod": { 15 | get: { 16 | responses: { 17 | "200": { 18 | description: "Successful greeting response", 19 | content: { 20 | "text/plain": { 21 | schema: { type: "string", example: "Hello Steven!" }, 22 | }, 23 | }, 24 | }, 25 | "400": { 26 | description: "Zod Error", 27 | content: { 28 | "application/json": { 29 | schema: { $ref: "#/components/schemas/Bad Request" }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | operationId: "getZod", 35 | parameters: [ 36 | { 37 | in: "query", 38 | name: "name", 39 | schema: { $ref: "#/components/schemas/name" }, 40 | }, 41 | ], 42 | description: "Say hello to the user", 43 | }, 44 | post: { 45 | responses: { 46 | "200": { 47 | description: "Successful user creation response", 48 | content: { 49 | "text/plain": { 50 | schema: { 51 | type: "string", 52 | example: "Hello Steven! Your id is 123", 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | operationId: "postZod", 59 | parameters: [ 60 | { 61 | in: "query", 62 | name: "name", 63 | schema: { $ref: "#/components/schemas/name" }, 64 | }, 65 | ], 66 | description: "Create a new user", 67 | requestBody: { 68 | content: { 69 | "application/json": { 70 | schema: { 71 | type: "object", 72 | properties: { id: { type: "number", example: 123 } }, 73 | required: ["id"], 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | "/valibot": { 81 | get: { 82 | responses: { 83 | "200": { 84 | description: "Successful greeting response", 85 | content: { "text/plain": { schema: { type: "string" } } }, 86 | }, 87 | "400": { 88 | description: "Zod Error", 89 | content: { 90 | "application/json": { 91 | schema: { $ref: "#/components/schemas/Bad Request" }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | operationId: "getValibot", 97 | parameters: [ 98 | { 99 | in: "query", 100 | name: "name", 101 | schema: { type: "string" }, 102 | required: true, 103 | }, 104 | ], 105 | description: "Say hello to the user", 106 | }, 107 | post: { 108 | responses: { 109 | "200": { 110 | description: "Successful user creation response", 111 | content: { 112 | "text/plain": { 113 | schema: { 114 | type: "string", 115 | example: "Hello Steven! Your id is 123", 116 | }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | operationId: "postValibot", 122 | parameters: [ 123 | { 124 | in: "query", 125 | name: "name", 126 | schema: { type: "string" }, 127 | required: true, 128 | }, 129 | ], 130 | description: "Create a new user", 131 | requestBody: { 132 | content: { 133 | "application/json": { 134 | schema: { 135 | type: "object", 136 | properties: { id: { type: "number" } }, 137 | required: ["id"], 138 | additionalProperties: false, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | }, 145 | }, 146 | components: { 147 | schemas: { 148 | "Bad Request": { 149 | type: "object", 150 | properties: { 151 | status: { type: "number", const: 400 }, 152 | message: { type: "string" }, 153 | }, 154 | required: ["status", "message"], 155 | }, 156 | name: { type: "string", example: "Steven", description: "User Name" }, 157 | }, 158 | }, 159 | }); 160 | 161 | it("should return an instance", () => { 162 | expect(instance).toBeDefined(); 163 | }); 164 | 165 | it("should initialize", async () => { 166 | const response = await instance?.request("/initialize", { 167 | method: "POST", 168 | body: JSON.stringify({ 169 | jsonrpc: "2.0", 170 | id: 0, 171 | method: "initialize", 172 | params: { 173 | protocolVersion: "2024-11-05", 174 | capabilities: { sampling: {}, roots: { listChanged: true } }, 175 | clientInfo: { name: "mcp-inspector", version: "0.7.0" }, 176 | }, 177 | }), 178 | headers: { 179 | "content-type": "application/json", 180 | }, 181 | }); 182 | 183 | const json = await response?.json(); 184 | 185 | expect(json).toBeDefined(); 186 | expect(json.result).toMatchObject({ 187 | protocolVersion: "2024-11-05", 188 | serverInfo: { 189 | name: "Muppet", 190 | version: "1.0.0", 191 | }, 192 | capabilities: { tools: {} }, 193 | }); 194 | }); 195 | 196 | it("should list tools", async () => { 197 | const response = await instance?.request("/tools/list", { 198 | method: "POST", 199 | body: JSON.stringify({ 200 | jsonrpc: "2.0", 201 | id: 1, 202 | method: "tools/list", 203 | params: {}, 204 | }), 205 | headers: { 206 | "content-type": "application/json", 207 | }, 208 | }); 209 | 210 | const json = await response?.json(); 211 | 212 | expect(json).toBeDefined(); 213 | expect(json.result).toMatchObject({ 214 | tools: [ 215 | { 216 | name: "getZod", 217 | description: "Say hello to the user", 218 | inputSchema: { 219 | type: "object", 220 | properties: { name: { type: "string" } }, 221 | additionalProperties: false, 222 | }, 223 | }, 224 | { 225 | name: "postZod", 226 | description: "Create a new user", 227 | inputSchema: { 228 | type: "object", 229 | properties: { name: { type: "string" } }, 230 | additionalProperties: false, 231 | }, 232 | }, 233 | { 234 | name: "getValibot", 235 | description: "Say hello to the user", 236 | inputSchema: { 237 | type: "object", 238 | properties: { name: { type: "string" } }, 239 | required: ["name"], 240 | additionalProperties: false, 241 | }, 242 | }, 243 | { 244 | name: "postValibot", 245 | description: "Create a new user", 246 | inputSchema: { 247 | type: "object", 248 | properties: { name: { type: "string" } }, 249 | required: ["name"], 250 | additionalProperties: false, 251 | }, 252 | }, 253 | ], 254 | }); 255 | }); 256 | 257 | it.skip("should call the tool", async () => { 258 | const response = await instance?.request("/tools/call", { 259 | method: "POST", 260 | body: JSON.stringify({ 261 | jsonrpc: "2.0", 262 | id: 2, 263 | method: "tools/call", 264 | params: { 265 | _meta: { progressToken: 0 }, 266 | name: describe.name, 267 | arguments: { name: "muppet" }, 268 | }, 269 | }), 270 | headers: { 271 | "content-type": "application/json", 272 | }, 273 | }); 274 | 275 | const json = await response?.json(); 276 | 277 | expect(json).toBeDefined(); 278 | expect(json.result).toMatchObject({ 279 | contents: [ 280 | { 281 | type: "text", 282 | text: "Hello World!", 283 | }, 284 | ], 285 | }); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/prompts.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { Hono } from "hono"; 3 | import z from "zod"; 4 | import { 5 | describePrompt, 6 | mValidator, 7 | type PromptResponseType, 8 | muppet, 9 | } from "../index"; 10 | 11 | describe("prompts", async () => { 12 | describe("with json validation", async () => { 13 | const about = { 14 | name: "json", 15 | description: "with json validation", 16 | }; 17 | 18 | const app = new Hono().post( 19 | "/", 20 | describePrompt(about), 21 | mValidator( 22 | "json", 23 | z.object({ 24 | search: z.string().optional(), 25 | }), 26 | ), 27 | (c) => { 28 | return c.json([ 29 | { 30 | role: "user", 31 | content: { 32 | type: "text", 33 | text: "Hello World!", 34 | }, 35 | }, 36 | ]); 37 | }, 38 | ); 39 | 40 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 41 | 42 | it("should list prompts", async () => { 43 | const response = await instance?.request("/prompts/list", { 44 | method: "POST", 45 | body: JSON.stringify({ 46 | jsonrpc: "2.0", 47 | id: 1, 48 | method: "prompts/list", 49 | params: {}, 50 | }), 51 | headers: { 52 | "content-type": "application/json", 53 | }, 54 | }); 55 | 56 | const json = await response?.json(); 57 | 58 | expect(json).toBeDefined(); 59 | expect(json.result).toMatchObject({ 60 | prompts: [about], 61 | }); 62 | }); 63 | 64 | it("should get the prompt", async () => { 65 | const response = await instance?.request("/prompts/get", { 66 | method: "POST", 67 | body: JSON.stringify({ 68 | jsonrpc: "2.0", 69 | id: 2, 70 | method: "prompts/get", 71 | params: { 72 | _meta: { progressToken: 0 }, 73 | name: about.name, 74 | arguments: { name: "muppet" }, 75 | }, 76 | }), 77 | headers: { 78 | "content-type": "application/json", 79 | }, 80 | }); 81 | 82 | const json = await response?.json(); 83 | 84 | expect(json).toBeDefined(); 85 | expect(json.result).toMatchObject({ 86 | description: about.description, 87 | messages: [ 88 | { 89 | role: "user", 90 | content: { 91 | type: "text", 92 | text: "Hello World!", 93 | }, 94 | }, 95 | ], 96 | }); 97 | }); 98 | }); 99 | 100 | describe("with query validation", async () => { 101 | const about = { 102 | name: "query", 103 | description: "with query validation", 104 | }; 105 | 106 | const app = new Hono().get( 107 | "/", 108 | describePrompt(about), 109 | mValidator( 110 | "query", 111 | z.object({ 112 | search: z.string().optional(), 113 | }), 114 | ), 115 | (c) => { 116 | return c.json([ 117 | { 118 | role: "user", 119 | content: { 120 | type: "text", 121 | text: "Hello World!", 122 | }, 123 | }, 124 | ]); 125 | }, 126 | ); 127 | 128 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 129 | 130 | it("should list prompts", async () => { 131 | const response = await instance?.request("/prompts/list", { 132 | method: "POST", 133 | body: JSON.stringify({ 134 | jsonrpc: "2.0", 135 | id: 1, 136 | method: "prompts/list", 137 | params: {}, 138 | }), 139 | headers: { 140 | "content-type": "application/json", 141 | }, 142 | }); 143 | 144 | const json = await response?.json(); 145 | 146 | expect(json).toBeDefined(); 147 | expect(json.result).toMatchObject({ 148 | prompts: [about], 149 | }); 150 | }); 151 | 152 | it("should get the prompt", async () => { 153 | const response = await instance?.request("/prompts/get", { 154 | method: "POST", 155 | body: JSON.stringify({ 156 | jsonrpc: "2.0", 157 | id: 2, 158 | method: "prompts/get", 159 | params: { 160 | _meta: { progressToken: 0 }, 161 | name: about.name, 162 | }, 163 | }), 164 | headers: { 165 | "content-type": "application/json", 166 | }, 167 | }); 168 | 169 | const json = await response?.json(); 170 | 171 | expect(json).toBeDefined(); 172 | expect(json.result).toMatchObject({ 173 | description: about.description, 174 | messages: [ 175 | { 176 | role: "user", 177 | content: { 178 | type: "text", 179 | text: "Hello World!", 180 | }, 181 | }, 182 | ], 183 | }); 184 | }); 185 | }); 186 | 187 | describe("with query and json validation", async () => { 188 | const about = { 189 | name: "query & json", 190 | description: "with query & json validation", 191 | }; 192 | 193 | const app = new Hono().post( 194 | "/", 195 | describePrompt(about), 196 | mValidator( 197 | "query", 198 | z.object({ 199 | search: z.string().optional(), 200 | }), 201 | ), 202 | mValidator( 203 | "json", 204 | z.object({ 205 | name: z.string(), 206 | }), 207 | ), 208 | (c) => { 209 | return c.json([ 210 | { 211 | role: "user", 212 | content: { 213 | type: "text", 214 | text: "Hello World!", 215 | }, 216 | }, 217 | ]); 218 | }, 219 | ); 220 | 221 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 222 | 223 | it("should list prompts", async () => { 224 | const response = await instance?.request("/prompts/list", { 225 | method: "POST", 226 | body: JSON.stringify({ 227 | jsonrpc: "2.0", 228 | id: 1, 229 | method: "prompts/list", 230 | params: {}, 231 | }), 232 | headers: { 233 | "content-type": "application/json", 234 | }, 235 | }); 236 | 237 | const json = await response?.json(); 238 | 239 | expect(json).toBeDefined(); 240 | expect(json.result).toMatchObject({ 241 | prompts: [about], 242 | }); 243 | }); 244 | 245 | it("should get the prompt", async () => { 246 | const response = await instance?.request("/prompts/get", { 247 | method: "POST", 248 | body: JSON.stringify({ 249 | jsonrpc: "2.0", 250 | id: 2, 251 | method: "prompts/get", 252 | params: { 253 | _meta: { progressToken: 0 }, 254 | name: about.name, 255 | arguments: { name: "muppet" }, 256 | }, 257 | }), 258 | headers: { 259 | "content-type": "application/json", 260 | }, 261 | }); 262 | 263 | const json = await response?.json(); 264 | 265 | expect(json).toBeDefined(); 266 | expect(json.result).toMatchObject({ 267 | description: about.description, 268 | messages: [ 269 | { 270 | role: "user", 271 | content: { 272 | type: "text", 273 | text: "Hello World!", 274 | }, 275 | }, 276 | ], 277 | }); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { Hono } from "hono"; 3 | import { registerResources, muppet, type Resource } from "../index"; 4 | 5 | describe("resources", async () => { 6 | describe("only static resources", async () => { 7 | const resource = { 8 | uri: "https://lorem.ipsum", 9 | name: "Returns Lorem ipsum", 10 | mimeType: "text/plain", 11 | }; 12 | 13 | const resolvedResource = { uri: "text:something", text: "Lorem ipsum" }; 14 | 15 | const app = new Hono().post( 16 | "/", 17 | registerResources(() => { 18 | return [resource]; 19 | }), 20 | ); 21 | 22 | const instance = await muppet(app, { 23 | name: "basic", 24 | version: "1.0.0", 25 | resources: { 26 | https: () => [resolvedResource], 27 | }, 28 | }); 29 | 30 | it("should list resources", async () => { 31 | const response = await instance?.request("/resources/list", { 32 | method: "POST", 33 | body: JSON.stringify({ 34 | jsonrpc: "2.0", 35 | id: 1, 36 | method: "resources/list", 37 | params: {}, 38 | }), 39 | headers: { 40 | "content-type": "application/json", 41 | }, 42 | }); 43 | 44 | const json = await response?.json(); 45 | 46 | expect(json).toBeDefined(); 47 | expect(json.result).toMatchObject({ 48 | resources: [resource], 49 | }); 50 | }); 51 | 52 | it("should read the resource", async () => { 53 | const response = await instance?.request("/resources/read", { 54 | method: "POST", 55 | body: JSON.stringify({ 56 | jsonrpc: "2.0", 57 | id: 2, 58 | method: "resources/read", 59 | params: { 60 | uri: resource.uri, 61 | }, 62 | }), 63 | headers: { 64 | "content-type": "application/json", 65 | }, 66 | }); 67 | 68 | const json = await response?.json(); 69 | 70 | expect(json).toBeDefined(); 71 | expect(json.result).toMatchObject({ 72 | contents: [resolvedResource], 73 | }); 74 | }); 75 | }); 76 | 77 | describe("only dynamic resources", async () => { 78 | const resource: Resource = { 79 | type: "template", 80 | uri: "https://lorem.{ending}", 81 | name: "Returns Lorem ipsum", 82 | mimeType: "text/plain", 83 | }; 84 | 85 | const resolvedResource = { uri: "text:something", text: "Lorem ipsum" }; 86 | 87 | const app = new Hono().post( 88 | "/", 89 | registerResources(() => { 90 | return [resource]; 91 | }), 92 | ); 93 | 94 | const instance = await muppet(app, { 95 | name: "basic", 96 | version: "1.0.0", 97 | resources: { 98 | https: () => [resolvedResource], 99 | }, 100 | }); 101 | 102 | it("should list template resources", async () => { 103 | const response = await instance?.request("/resources/templates/list", { 104 | method: "POST", 105 | body: JSON.stringify({ 106 | jsonrpc: "2.0", 107 | id: 1, 108 | method: "resources/templates/list", 109 | params: {}, 110 | }), 111 | headers: { 112 | "content-type": "application/json", 113 | }, 114 | }); 115 | 116 | const json = await response?.json(); 117 | 118 | expect(json).toBeDefined(); 119 | expect(json.result).toMatchObject({ 120 | resourceTemplates: [ 121 | { 122 | uriTemplate: resource.uri, 123 | name: resource.name, 124 | mimeType: resource.mimeType, 125 | }, 126 | ], 127 | }); 128 | }); 129 | 130 | it("should read the template resource", async () => { 131 | const response = await instance?.request("/resources/read", { 132 | method: "POST", 133 | body: JSON.stringify({ 134 | jsonrpc: "2.0", 135 | id: 2, 136 | method: "resources/read", 137 | params: { 138 | uri: "https://lorem.random", 139 | }, 140 | }), 141 | headers: { 142 | "content-type": "application/json", 143 | }, 144 | }); 145 | 146 | const json = await response?.json(); 147 | 148 | expect(json).toBeDefined(); 149 | expect(json.result).toMatchObject({ 150 | contents: [resolvedResource], 151 | }); 152 | }); 153 | }); 154 | 155 | describe("mixed resources", async () => { 156 | const mixedResources: Resource[] = [ 157 | { 158 | uri: "https://lorem.ipsum", 159 | name: "Returns Lorem ipsum", 160 | mimeType: "text/plain", 161 | }, 162 | { 163 | type: "template", 164 | uri: "https://lorem.{ending}", 165 | name: "Returns Lorem ipsum", 166 | mimeType: "text/plain", 167 | }, 168 | ]; 169 | 170 | const resolvedResource = { uri: "text:something", text: "Lorem ipsum" }; 171 | 172 | const app = new Hono().post( 173 | "/", 174 | registerResources(() => { 175 | return mixedResources; 176 | }), 177 | ); 178 | 179 | const instance = await muppet(app, { 180 | name: "basic", 181 | version: "1.0.0", 182 | resources: { 183 | https: () => [resolvedResource], 184 | }, 185 | }); 186 | 187 | it("should list static resources", async () => { 188 | const response = await instance?.request("/resources/list", { 189 | method: "POST", 190 | body: JSON.stringify({ 191 | jsonrpc: "2.0", 192 | id: 1, 193 | method: "resources/list", 194 | params: {}, 195 | }), 196 | headers: { 197 | "content-type": "application/json", 198 | }, 199 | }); 200 | 201 | const json = await response?.json(); 202 | 203 | expect(json).toBeDefined(); 204 | expect(json.result).toMatchObject({ 205 | resources: [mixedResources[0]], 206 | }); 207 | }); 208 | 209 | it("should read the static resource", async () => { 210 | const response = await instance?.request("/resources/read", { 211 | method: "POST", 212 | body: JSON.stringify({ 213 | jsonrpc: "2.0", 214 | id: 2, 215 | method: "resources/read", 216 | params: { 217 | uri: mixedResources[0].uri, 218 | }, 219 | }), 220 | headers: { 221 | "content-type": "application/json", 222 | }, 223 | }); 224 | 225 | const json = await response?.json(); 226 | 227 | expect(json).toBeDefined(); 228 | expect(json.result).toMatchObject({ 229 | contents: [resolvedResource], 230 | }); 231 | }); 232 | 233 | it("should list template resources", async () => { 234 | const response = await instance?.request("/resources/templates/list", { 235 | method: "POST", 236 | body: JSON.stringify({ 237 | jsonrpc: "2.0", 238 | id: 1, 239 | method: "resources/templates/list", 240 | params: {}, 241 | }), 242 | headers: { 243 | "content-type": "application/json", 244 | }, 245 | }); 246 | 247 | const json = await response?.json(); 248 | 249 | expect(json).toBeDefined(); 250 | expect(json.result).toMatchObject({ 251 | resourceTemplates: [ 252 | { 253 | uriTemplate: mixedResources[1].uri, 254 | name: mixedResources[1].name, 255 | mimeType: mixedResources[1].mimeType, 256 | }, 257 | ], 258 | }); 259 | }); 260 | 261 | it("should read the template resource", async () => { 262 | const response = await instance?.request("/resources/read", { 263 | method: "POST", 264 | body: JSON.stringify({ 265 | jsonrpc: "2.0", 266 | id: 2, 267 | method: "resources/read", 268 | params: { 269 | uri: mixedResources[0].uri, 270 | }, 271 | }), 272 | headers: { 273 | "content-type": "application/json", 274 | }, 275 | }); 276 | 277 | const json = await response?.json(); 278 | 279 | expect(json).toBeDefined(); 280 | expect(json.result).toMatchObject({ 281 | contents: [resolvedResource], 282 | }); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/tools.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { Hono } from "hono"; 3 | import z from "zod"; 4 | import { 5 | describeTool, 6 | mValidator, 7 | type ToolResponseType, 8 | muppet, 9 | } from "../index"; 10 | 11 | describe("tools", async () => { 12 | describe("with json validation", async () => { 13 | const about = { 14 | name: "json", 15 | description: "with json validation", 16 | }; 17 | 18 | const app = new Hono().post( 19 | "/", 20 | describeTool(about), 21 | mValidator( 22 | "json", 23 | z.object({ 24 | search: z.string().optional(), 25 | }), 26 | ), 27 | (c) => { 28 | return c.json([ 29 | { 30 | type: "text", 31 | text: "Hello World!", 32 | }, 33 | ]); 34 | }, 35 | ); 36 | 37 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 38 | 39 | it("should list tools", async () => { 40 | const response = await instance?.request("/tools/list", { 41 | method: "POST", 42 | body: JSON.stringify({ 43 | jsonrpc: "2.0", 44 | id: 1, 45 | method: "tools/list", 46 | params: {}, 47 | }), 48 | headers: { 49 | "content-type": "application/json", 50 | }, 51 | }); 52 | 53 | const json = await response?.json(); 54 | 55 | expect(json).toBeDefined(); 56 | expect(json.result).toMatchObject({ 57 | tools: [ 58 | { 59 | ...about, 60 | inputSchema: { 61 | type: "object", 62 | properties: { search: { type: "string" } }, 63 | additionalProperties: false, 64 | $schema: "http://json-schema.org/draft-07/schema#", 65 | }, 66 | }, 67 | ], 68 | }); 69 | }); 70 | 71 | it("should call the tool", async () => { 72 | const response = await instance?.request("/tools/call", { 73 | method: "POST", 74 | body: JSON.stringify({ 75 | jsonrpc: "2.0", 76 | id: 2, 77 | method: "tools/call", 78 | params: { 79 | _meta: { progressToken: 0 }, 80 | name: about.name, 81 | arguments: { name: "muppet" }, 82 | }, 83 | }), 84 | headers: { 85 | "content-type": "application/json", 86 | }, 87 | }); 88 | 89 | const json = await response?.json(); 90 | 91 | expect(json).toBeDefined(); 92 | expect(json.result).toMatchObject({ 93 | content: [ 94 | { 95 | type: "text", 96 | text: "Hello World!", 97 | }, 98 | ], 99 | }); 100 | }); 101 | }); 102 | 103 | describe("with query validation", async () => { 104 | const about = { 105 | name: "query", 106 | description: "with query validation", 107 | }; 108 | 109 | const app = new Hono().get( 110 | "/", 111 | describeTool(about), 112 | mValidator( 113 | "query", 114 | z.object({ 115 | search: z.string().optional(), 116 | }), 117 | ), 118 | (c) => { 119 | return c.json([ 120 | { 121 | type: "text", 122 | text: "Hello World!", 123 | }, 124 | ]); 125 | }, 126 | ); 127 | 128 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 129 | 130 | it("should list tools", async () => { 131 | const response = await instance?.request("/tools/list", { 132 | method: "POST", 133 | body: JSON.stringify({ 134 | jsonrpc: "2.0", 135 | id: 1, 136 | method: "tools/list", 137 | params: {}, 138 | }), 139 | headers: { 140 | "content-type": "application/json", 141 | }, 142 | }); 143 | 144 | const json = await response?.json(); 145 | 146 | expect(json).toBeDefined(); 147 | expect(json.result).toMatchObject({ 148 | tools: [ 149 | { 150 | ...about, 151 | inputSchema: { 152 | type: "object", 153 | properties: { search: { type: "string" } }, 154 | additionalProperties: false, 155 | $schema: "http://json-schema.org/draft-07/schema#", 156 | }, 157 | }, 158 | ], 159 | }); 160 | }); 161 | 162 | it("should call the tool", async () => { 163 | const response = await instance?.request("/tools/call", { 164 | method: "POST", 165 | body: JSON.stringify({ 166 | jsonrpc: "2.0", 167 | id: 2, 168 | method: "tools/call", 169 | params: { 170 | _meta: { progressToken: 0 }, 171 | name: about.name, 172 | }, 173 | }), 174 | headers: { 175 | "content-type": "application/json", 176 | }, 177 | }); 178 | 179 | const json = await response?.json(); 180 | 181 | expect(json).toBeDefined(); 182 | expect(json.result).toMatchObject({ 183 | content: [ 184 | { 185 | type: "text", 186 | text: "Hello World!", 187 | }, 188 | ], 189 | }); 190 | }); 191 | }); 192 | 193 | describe("with query and json validation", async () => { 194 | const about = { 195 | name: "query & json", 196 | description: "with query & json validation", 197 | }; 198 | 199 | const app = new Hono().post( 200 | "/", 201 | describeTool(about), 202 | mValidator( 203 | "query", 204 | z.object({ 205 | search: z.string().optional(), 206 | }), 207 | ), 208 | mValidator( 209 | "json", 210 | z.object({ 211 | name: z.string(), 212 | }), 213 | ), 214 | (c) => { 215 | return c.json([ 216 | { 217 | type: "text", 218 | text: "Hello World!", 219 | }, 220 | ]); 221 | }, 222 | ); 223 | 224 | const instance = await muppet(app, { name: "basic", version: "1.0.0" }); 225 | 226 | it("should list tools", async () => { 227 | const response = await instance?.request("/tools/list", { 228 | method: "POST", 229 | body: JSON.stringify({ 230 | jsonrpc: "2.0", 231 | id: 1, 232 | method: "tools/list", 233 | params: {}, 234 | }), 235 | headers: { 236 | "content-type": "application/json", 237 | }, 238 | }); 239 | 240 | const json = await response?.json(); 241 | 242 | expect(json).toBeDefined(); 243 | expect(json.result).toMatchObject({ 244 | tools: [ 245 | { 246 | ...about, 247 | inputSchema: { 248 | type: "object", 249 | properties: { 250 | search: { type: "string" }, 251 | name: { type: "string" }, 252 | }, 253 | required: ["name"], 254 | additionalProperties: false, 255 | $schema: "http://json-schema.org/draft-07/schema#", 256 | }, 257 | }, 258 | ], 259 | }); 260 | }); 261 | 262 | it("should call the tool", async () => { 263 | const response = await instance?.request("/tools/call", { 264 | method: "POST", 265 | body: JSON.stringify({ 266 | jsonrpc: "2.0", 267 | id: 2, 268 | method: "tools/call", 269 | params: { 270 | _meta: { progressToken: 0 }, 271 | name: about.name, 272 | arguments: { name: "muppet" }, 273 | }, 274 | }), 275 | headers: { 276 | "content-type": "application/json", 277 | }, 278 | }); 279 | 280 | const json = await response?.json(); 281 | 282 | expect(json).toBeDefined(); 283 | expect(json.result).toMatchObject({ 284 | content: [ 285 | { 286 | type: "text", 287 | text: "Hello World!", 288 | }, 289 | ], 290 | }); 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /packages/core/src/bridge.ts: -------------------------------------------------------------------------------- 1 | import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; 2 | import { 3 | RequestSchema, 4 | type JSONRPCMessage, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | import type { Env, Hono, Schema } from "hono"; 7 | import type { BlankSchema } from "hono/types"; 8 | import type { Logger } from "pino"; 9 | 10 | type Promisify = T | Promise; 11 | 12 | export type BridgeOptions< 13 | E extends Env = any, 14 | S extends Schema = BlankSchema, 15 | P extends string = string, 16 | > = { 17 | /** 18 | * The Muppet app instance 19 | */ 20 | mcp: Promisify>; 21 | /** 22 | * The transport that will be used to send and receive messages 23 | */ 24 | transport: Transport; 25 | /** 26 | * The logger that will be used to log messages 27 | */ 28 | logger?: Logger; 29 | }; 30 | 31 | /** 32 | * Bridge aka Bifrost, the connection between the app and the transport 33 | * @param options 34 | */ 35 | export function bridge(options: BridgeOptions) { 36 | const { mcp: app, transport, logger } = options; 37 | 38 | let messageId = 0; 39 | 40 | transport.onmessage = async (message) => { 41 | const payload = await handleMessage({ 42 | mcp: await app, 43 | message, 44 | logger, 45 | }); 46 | 47 | if ("method" in message && message.method === "initialize") messageId = -1; 48 | 49 | if (payload) { 50 | messageId++; 51 | 52 | await transport 53 | .send({ 54 | ...payload, 55 | id: messageId, 56 | } as JSONRPCMessage) 57 | .then(() => logger?.info("Sent response")) 58 | .catch((error) => logger?.error(error, "Failed to send cancellation")); 59 | } 60 | }; 61 | 62 | return transport.start(); 63 | } 64 | 65 | export type HandleMessageOptions< 66 | E extends Env = any, 67 | S extends Schema = BlankSchema, 68 | P extends string = string, 69 | > = { 70 | /** 71 | * The Muppet app instance 72 | */ 73 | mcp: Hono; 74 | /** 75 | * The message received from the MCP Client 76 | */ 77 | message: unknown; 78 | /** 79 | * The logger that will be used to log messages 80 | */ 81 | logger?: Logger; 82 | }; 83 | 84 | /** 85 | * Processes the message and generates the response for the MCPs. 86 | * @param options 87 | */ 88 | export async function handleMessage(options: HandleMessageOptions) { 89 | const { mcp: app, message, logger } = options; 90 | 91 | logger?.info( 92 | { message, string: JSON.stringify(message) }, 93 | "Received message", 94 | ); 95 | 96 | const validatedMessage = RequestSchema.parse(message); 97 | 98 | const response = await app.request(validatedMessage.method, { 99 | method: "POST", 100 | body: JSON.stringify(message), 101 | headers: { 102 | "content-type": "application/json", 103 | }, 104 | }); 105 | 106 | // If there's no payload, we don't need to send a response. Eg. Notifications 107 | if (response.status === 204) return null; 108 | 109 | const payload = (await response.json()) as JSONRPCMessage; 110 | payload.jsonrpc = "2.0"; 111 | 112 | logger?.info({ payload }, "Response payload"); 113 | 114 | return payload; 115 | } 116 | -------------------------------------------------------------------------------- /packages/core/src/describe.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareHandler } from "hono/types"; 2 | import type { 3 | DescribeOptions, 4 | PromptDescribeOptions, 5 | ToolDescribeOptions, 6 | } from "./types.js"; 7 | import { 8 | McpPrimitives, 9 | type McpPrimitivesValue, 10 | uniqueSymbol, 11 | } from "./utils.js"; 12 | 13 | function describeRoute( 14 | type: McpPrimitivesValue, 15 | ) { 16 | return (docs?: T): MiddlewareHandler => { 17 | const middleware: MiddlewareHandler = async (_c, next) => { 18 | await next(); 19 | }; 20 | 21 | return Object.assign(middleware, { 22 | [uniqueSymbol]: { 23 | toJson: docs ?? {}, 24 | type, 25 | }, 26 | }); 27 | }; 28 | } 29 | 30 | /** 31 | * Describe prompt's name and description 32 | */ 33 | export const describePrompt = describeRoute( 34 | McpPrimitives.PROMPTS, 35 | ); 36 | 37 | /** 38 | * Describe tool's name and description 39 | */ 40 | export const describeTool = describeRoute( 41 | McpPrimitives.TOOLS, 42 | ); 43 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./describe.js"; 2 | export { muppet } from "./muppet.js"; 3 | export * from "./resources.js"; 4 | export type * from "./types.js"; 5 | export * from "./utils.js"; 6 | export * from "./validator.js"; 7 | export * from "./bridge.js"; 8 | -------------------------------------------------------------------------------- /packages/core/src/muppet.ts: -------------------------------------------------------------------------------- 1 | import { sValidator } from "@hono/standard-validator"; 2 | import { 3 | CallToolRequestSchema, 4 | CompleteRequestSchema, 5 | ErrorCode, 6 | GetPromptRequestSchema, 7 | InitializeRequestSchema, 8 | LATEST_PROTOCOL_VERSION, 9 | ReadResourceRequestSchema, 10 | SUPPORTED_PROTOCOL_VERSIONS, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import { Hono, type Context } from "hono"; 13 | import type { 14 | BlankEnv, 15 | BlankSchema, 16 | Env, 17 | Input, 18 | Schema, 19 | ValidationTargets, 20 | } from "hono/types"; 21 | import type { JSONSchema7 } from "json-schema"; 22 | import type { 23 | ClientToServerNotifications, 24 | BaseEnv, 25 | CompletionFn, 26 | ConceptConfiguration, 27 | CreateMuppetOptions, 28 | DescribeOptions, 29 | MuppetConfiguration, 30 | PromptConfiguration, 31 | Resource, 32 | ServerConfiguration, 33 | ToolHandlerResponse, 34 | } from "./types"; 35 | import { McpPrimitives, uniqueSymbol } from "./utils"; 36 | 37 | export async function muppet< 38 | E extends Env = BlankEnv, 39 | S extends Schema = BlankSchema, 40 | P extends string = string, 41 | >(hono: Hono, config: MuppetConfiguration) { 42 | const specs = await generateSpecs(hono, config.symbols); 43 | 44 | return createMuppetServer({ 45 | config, 46 | specs, 47 | app: hono, 48 | }); 49 | } 50 | 51 | export function createMuppetServer< 52 | E extends Env = BlankEnv, 53 | S extends Schema = BlankSchema, 54 | P extends string = string, 55 | >(options: CreateMuppetOptions): Hono> { 56 | const { config, specs, app } = options; 57 | 58 | /** 59 | * Tools router 60 | */ 61 | const toolsRouter = new Hono>() 62 | .use(async (c, next) => { 63 | if (!(McpPrimitives.TOOLS in c.get("specs"))) { 64 | throw new Error("No tools available"); 65 | } 66 | 67 | await next(); 68 | }) 69 | .post("/list", (c) => 70 | c.json({ 71 | result: { 72 | tools: Object.values(c.get("specs").tools ?? {}).map( 73 | ({ name, description, inputSchema }) => ({ 74 | name, 75 | description, 76 | inputSchema, 77 | }), 78 | ), 79 | }, 80 | }), 81 | ) 82 | .post("/call", sValidator("json", CallToolRequestSchema), async (c) => { 83 | const { params } = c.req.valid("json"); 84 | 85 | const _tool = c.get("specs").tools?.[params.name]; 86 | 87 | if (!_tool) { 88 | throw new Error("Unable to find the path for the tool!"); 89 | } 90 | 91 | const res = await c.get("app").request( 92 | ...getRequestInit({ 93 | path: _tool.path, 94 | method: _tool.method, 95 | schema: _tool.schema, 96 | args: params.arguments, 97 | }), 98 | createMuppetEnv(c), 99 | ); 100 | 101 | const json = await res.json(); 102 | 103 | if (_tool.resourceType === "text") { 104 | return c.json({ 105 | result: { 106 | content: [ 107 | { 108 | type: "text", 109 | text: typeof json === "string" ? json : JSON.stringify(json), 110 | }, 111 | ], 112 | }, 113 | }); 114 | } 115 | 116 | if (Array.isArray(json)) 117 | return c.json({ 118 | result: { content: json }, 119 | }); 120 | 121 | return c.json({ result: json }); 122 | }); 123 | 124 | /** 125 | * Prompt Router 126 | */ 127 | const promptsRouter = new Hono>() 128 | .use(async (c, next) => { 129 | if (!(McpPrimitives.PROMPTS in c.get("specs"))) { 130 | throw new Error("No prompts available"); 131 | } 132 | 133 | await next(); 134 | }) 135 | .post("/list", (c) => { 136 | return c.json({ 137 | result: { 138 | prompts: Object.values(c.get("specs").prompts ?? {}).map( 139 | ({ path, ...prompt }) => prompt, 140 | ), 141 | }, 142 | }); 143 | }) 144 | .post("/get", sValidator("json", GetPromptRequestSchema), async (c) => { 145 | const { params } = c.req.valid("json"); 146 | 147 | const prompt = c.get("specs").prompts?.[params.name]; 148 | 149 | if (!prompt) { 150 | throw new Error("Unable to find the path for the prompt!"); 151 | } 152 | 153 | const res = await c.get("app").request( 154 | ...getRequestInit({ 155 | path: prompt.path, 156 | method: prompt.method, 157 | schema: prompt.schema, 158 | args: params.arguments, 159 | }), 160 | createMuppetEnv(c), 161 | ); 162 | 163 | const json = await res.json(); 164 | 165 | if (Array.isArray(json)) 166 | return c.json({ 167 | result: { description: prompt.description, messages: json }, 168 | }); 169 | 170 | return c.json({ result: json }); 171 | }); 172 | 173 | /** 174 | * Resource Router 175 | */ 176 | const resourcesRouter = new Hono>() 177 | .use(async (c, next) => { 178 | if (!(McpPrimitives.RESOURCES in c.get("specs"))) { 179 | throw new Error("No resources available"); 180 | } 181 | 182 | await next(); 183 | }) 184 | .post("/list", async (c) => { 185 | const resources = await findAllTheResources(c, (resource) => { 186 | if (resource.type !== "template") { 187 | return { 188 | name: resource.name, 189 | description: resource.description, 190 | mimeType: resource.mimeType, 191 | uri: resource.uri, 192 | }; 193 | } 194 | 195 | return; 196 | }); 197 | 198 | return c.json({ 199 | result: { 200 | resources, 201 | }, 202 | }); 203 | }) 204 | .post("/templates/list", async (c) => { 205 | const resources = await findAllTheResources(c, (resource) => { 206 | if (resource.type === "template") { 207 | return { 208 | name: resource.name, 209 | description: resource.description, 210 | mimeType: resource.mimeType, 211 | uriTemplate: resource.uri, 212 | }; 213 | } 214 | 215 | return; 216 | }); 217 | 218 | return c.json({ 219 | result: { 220 | resourceTemplates: resources, 221 | }, 222 | }); 223 | }) 224 | .post("/read", sValidator("json", ReadResourceRequestSchema), async (c) => { 225 | const { params } = c.req.valid("json"); 226 | 227 | const protocol = params.uri.split(":")[0]; 228 | const handler = c.get("muppet").resources?.[protocol]; 229 | 230 | if (!handler) { 231 | throw new Error(`Unable to find the handler for ${protocol} protocol!`); 232 | } 233 | 234 | const contents = await handler(params.uri); 235 | 236 | if (Array.isArray(contents)) 237 | return c.json({ 238 | result: { contents }, 239 | }); 240 | 241 | return c.json({ result: contents }); 242 | }); 243 | 244 | return new Hono>() 245 | .use(async (c, next) => { 246 | config.logger?.info( 247 | { method: c.req.method, path: c.req.path }, 248 | "Incoming request", 249 | ); 250 | 251 | c.set("muppet", config); 252 | c.set("specs", specs); 253 | c.set("app", app); 254 | 255 | await next(); 256 | 257 | config.logger?.info({ status: c.res.status }, "Outgoing response"); 258 | }) 259 | .post( 260 | "/initialize", 261 | sValidator("json", InitializeRequestSchema), 262 | async (c) => { 263 | const { params } = c.req.valid("json"); 264 | 265 | const { name, version } = c.get("muppet"); 266 | const specs = c.get("specs"); 267 | 268 | const hasTools = McpPrimitives.TOOLS in specs; 269 | const hasPrompts = McpPrimitives.PROMPTS in specs; 270 | const hasResources = McpPrimitives.RESOURCES in specs; 271 | 272 | return c.json({ 273 | result: { 274 | protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes( 275 | params?.protocolVersion, 276 | ) 277 | ? params.protocolVersion 278 | : LATEST_PROTOCOL_VERSION, 279 | serverInfo: { 280 | name, 281 | version, 282 | }, 283 | capabilities: { 284 | tools: hasTools ? {} : undefined, 285 | prompts: hasPrompts ? {} : undefined, 286 | resources: hasResources ? {} : undefined, 287 | }, 288 | }, 289 | }); 290 | }, 291 | ) 292 | .post("/notifications/:event", (c) => { 293 | c.get("muppet").events?.emit( 294 | c, 295 | `notifications/${c.req.param("event")}` as keyof ClientToServerNotifications, 296 | undefined, 297 | ); 298 | 299 | return c.body(null, 204); 300 | }) 301 | .post("/ping", (c) => c.json({ result: {} })) 302 | .route("/tools", toolsRouter) 303 | .route("/prompts", promptsRouter) 304 | .route("/resources", resourcesRouter) 305 | .post( 306 | "/completion/complete", 307 | sValidator("json", CompleteRequestSchema), 308 | async (c) => { 309 | const { params } = c.req.valid("json"); 310 | 311 | let completionFn: CompletionFn | undefined; 312 | 313 | if (params.ref.type === "ref/prompt") { 314 | completionFn = c.get("specs").prompts?.[params.ref.name].completion; 315 | } else if (params.ref.type === "ref/resource") { 316 | completionFn = await findAllTheResources(c, (resource) => { 317 | if ( 318 | resource.type === "template" && 319 | resource.uri === params.ref.uri 320 | ) { 321 | return resource.completion; 322 | } 323 | 324 | return; 325 | }).then((res) => res[0]); 326 | } 327 | 328 | if (!completionFn) 329 | return c.json({ 330 | result: { 331 | completion: { 332 | values: [], 333 | total: 0, 334 | hasMore: false, 335 | }, 336 | }, 337 | }); 338 | 339 | const values = await completionFn(params.argument); 340 | 341 | if (Array.isArray(values)) { 342 | return c.json({ 343 | result: { 344 | completion: { 345 | values, 346 | total: values.length, 347 | hasMore: false, 348 | }, 349 | }, 350 | }); 351 | } 352 | 353 | return c.json({ result: { completion: values } }); 354 | }, 355 | ) 356 | .notFound((c) => { 357 | c.get("muppet").logger?.info("Method not found"); 358 | 359 | return c.json({ 360 | error: { 361 | code: ErrorCode.MethodNotFound, 362 | message: "Method not found", 363 | }, 364 | }); 365 | }) 366 | .onError((err, c) => { 367 | c.get("muppet").logger?.error({ err }, "Internal error"); 368 | 369 | return c.json({ 370 | error: { 371 | // @ts-expect-error 372 | code: Number.isSafeInteger(err.code) 373 | ? // @ts-expect-error 374 | err.code 375 | : ErrorCode.InternalError, 376 | message: err.message ?? "Internal error", 377 | }, 378 | }); 379 | }); 380 | } 381 | 382 | export async function generateSpecs< 383 | E extends Env = BlankEnv, 384 | S extends Schema = BlankSchema, 385 | P extends string = string, 386 | >(hono: Hono, symbols: unknown[] = []) { 387 | const concepts: ConceptConfiguration = {}; 388 | 389 | // biome-ignore lint/suspicious/noExplicitAny: Need this for the type 390 | const _symbols: any[] = [...symbols, uniqueSymbol]; 391 | 392 | for (const route of hono.routes) { 393 | const uSymbol = _symbols.find((symbol) => symbol in route.handler); 394 | 395 | if (!uSymbol) continue; 396 | 397 | // @ts-expect-error 398 | const { validationTarget, toJson, type } = route.handler[ 399 | uSymbol 400 | ] as ToolHandlerResponse; 401 | 402 | let result: DescribeOptions | JSONSchema7; 403 | 404 | if (typeof toJson === "function") { 405 | result = await toJson(); 406 | } else { 407 | result = toJson ?? {}; 408 | } 409 | 410 | const concept = concepts[route.path]?.[route.method]; 411 | 412 | if (concept?.type && type && concept.type !== type) { 413 | throw new Error( 414 | `Conflicting types for ${route.path}: ${concept.type} and ${type}`, 415 | ); 416 | } 417 | 418 | let payload: Record = { 419 | ...(concept ?? {}), 420 | type: type ?? concept?.type, 421 | }; 422 | 423 | if (validationTarget && "schema" in result) { 424 | payload.schema = { 425 | ...(payload.schema ?? {}), 426 | [validationTarget]: result.schema, 427 | }; 428 | } else { 429 | payload = { 430 | ...payload, 431 | ...result, 432 | }; 433 | } 434 | 435 | concepts[route.path] = { 436 | ...(concepts[route.path] ?? {}), 437 | [route.method]: payload, 438 | }; 439 | } 440 | 441 | const configuration: ServerConfiguration = {}; 442 | for (const [path, conceptByMethod] of Object.entries(concepts)) { 443 | if (!conceptByMethod) continue; 444 | 445 | for (const [method, concept] of Object.entries(conceptByMethod)) { 446 | if (!concept) continue; 447 | 448 | if (!concept.type) { 449 | throw new Error(`Type not found for ${path}`); 450 | } 451 | 452 | if (concept.type === McpPrimitives.TOOLS) { 453 | if (!configuration.tools) { 454 | configuration.tools = {}; 455 | } 456 | 457 | const key = concept.name ?? generateKey(method, path); 458 | 459 | configuration.tools[key] = { 460 | name: key, 461 | description: concept.description, 462 | resourceType: concept.resourceType, 463 | inputSchema: mergeSchemas(concept.schema) ?? {}, 464 | path, 465 | method, 466 | schema: concept.schema, 467 | }; 468 | } else if (concept.type === McpPrimitives.PROMPTS) { 469 | if (!configuration.prompts) { 470 | configuration.prompts = {}; 471 | } 472 | 473 | const key = concept.name ?? generateKey(method, path); 474 | 475 | const args: PromptConfiguration[string]["arguments"] = []; 476 | const meragedSchema = mergeSchemas(concept.schema) ?? {}; 477 | 478 | for (const arg of Object.keys(meragedSchema.properties ?? {})) { 479 | args.push({ 480 | name: arg, 481 | // @ts-expect-error 482 | description: meragedSchema.properties?.[arg]?.description, 483 | required: meragedSchema.required?.includes(arg) ?? false, 484 | }); 485 | } 486 | 487 | configuration.prompts[key] = { 488 | name: key, 489 | description: concept.description, 490 | completion: concept.completion, 491 | arguments: args, 492 | path, 493 | method, 494 | schema: concept.schema, 495 | }; 496 | } else if (concept.type === McpPrimitives.RESOURCES) { 497 | if (!configuration.resources) { 498 | configuration.resources = {}; 499 | } 500 | 501 | configuration.resources[generateKey(method, path)] = { 502 | path, 503 | method, 504 | }; 505 | } 506 | } 507 | } 508 | 509 | return configuration; 510 | } 511 | 512 | export function mergeSchemas( 513 | schema?: { [K in keyof ValidationTargets]?: JSONSchema7 }, 514 | ) { 515 | let tmp: JSONSchema7 | undefined = undefined; 516 | 517 | for (const sch of Object.values(schema ?? {})) { 518 | if (!tmp) { 519 | tmp = sch; 520 | continue; 521 | } 522 | 523 | tmp = { 524 | ...tmp, 525 | properties: { 526 | ...tmp.properties, 527 | ...sch.properties, 528 | }, 529 | required: [...(tmp.required ?? []), ...(sch.required ?? [])], 530 | }; 531 | } 532 | 533 | return tmp; 534 | } 535 | 536 | async function findAllTheResources< 537 | T, 538 | E extends Env = BlankEnv, 539 | S extends Schema = BlankSchema, 540 | P extends string = string, 541 | >(c: Context>, mapFn: (resource: Resource) => T | undefined) { 542 | const responses = await Promise.all( 543 | Object.values(c.get("specs").resources ?? {}).map( 544 | async ({ path, method }) => 545 | // @ts-expect-error 546 | c.get("app").request(path, { 547 | method, 548 | headers: c.req.header(), 549 | }) as Promise, 550 | ), 551 | ); 552 | 553 | return responses.flat(2).reduce((prev: T[], resource: Resource) => { 554 | const mapped = mapFn(resource); 555 | 556 | if (mapped) prev.push(mapped); 557 | 558 | return prev; 559 | }, []); 560 | } 561 | 562 | export function generateKey(method: string, path: string) { 563 | return `${method}:${path}`; 564 | } 565 | 566 | type GetRequestInitOptions = { 567 | path: string; 568 | method: string; 569 | schema?: { 570 | [K in keyof ValidationTargets]?: JSONSchema7; 571 | }; 572 | args?: Record; 573 | }; 574 | 575 | function getRequestInit(options: GetRequestInitOptions): [string, RequestInit] { 576 | const { path, method, schema, args } = options; 577 | 578 | const targetProps: { 579 | [K in keyof ValidationTargets]?: Record; 580 | } = {}; 581 | const reqInit: RequestInit = { 582 | method, 583 | }; 584 | 585 | for (const [target, { properties }] of Object.entries(schema ?? {}) as [ 586 | keyof ValidationTargets, 587 | JSONSchema7, 588 | ][]) { 589 | if (!properties) continue; 590 | 591 | targetProps[target] = Object.keys(properties).reduce< 592 | Record 593 | >((prev, key) => { 594 | if (args?.[key] !== undefined) prev[key] = args?.[key]; 595 | return prev; 596 | }, {}); 597 | } 598 | 599 | if (Object.values(targetProps.header ?? {}).length > 0) { 600 | reqInit.headers = targetProps.header as Record; 601 | } 602 | if (Object.values(targetProps.json ?? {}).length > 0) { 603 | reqInit.body = JSON.stringify(targetProps.json); 604 | reqInit.headers = { 605 | ...reqInit.headers, 606 | "content-type": "application/json", 607 | }; 608 | } 609 | 610 | // Handle query params 611 | const queryString = querySerializer(targetProps.query); 612 | const pathWithParams = placeParamValues(path, targetProps.param); 613 | 614 | return [ 615 | `${pathWithParams}${queryString.length > 0 ? `?${queryString}` : ""}`, 616 | reqInit, 617 | ]; 618 | } 619 | 620 | function placeParamValues(path: string, params?: Record) { 621 | return path 622 | .split("/") 623 | .map((x) => { 624 | let tmp = x; 625 | if (tmp.startsWith(":")) { 626 | const match = tmp.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/); 627 | if (match) { 628 | const paramName = match[1]; 629 | const value = params?.[paramName]; 630 | 631 | if (value) tmp = String(value); 632 | } else { 633 | tmp = tmp.slice(1, tmp.length); 634 | if (tmp.endsWith("?")) tmp = tmp.slice(0, -1); 635 | } 636 | } 637 | 638 | return tmp; 639 | }) 640 | .join("/"); 641 | } 642 | 643 | type QuerySerializerOptions = { 644 | prefix?: string; 645 | separator?: string; 646 | }; 647 | 648 | function querySerializer( 649 | query?: Record, 650 | options?: QuerySerializerOptions, 651 | ) { 652 | const { prefix, separator = "__" } = options ?? {}; 653 | 654 | return Object.entries(query ?? {}) 655 | .reduce((prev, [key, value]) => { 656 | const uniqueKey = `${prefix ? `${prefix}${separator}` : ""}${key}`; 657 | if (value) { 658 | if (Array.isArray(value)) 659 | prev.push( 660 | ...value 661 | .filter((val) => val !== undefined) 662 | .map((val) => `${uniqueKey}=${val}`), 663 | ); 664 | else if (typeof value === "object") 665 | prev.push( 666 | querySerializer(value as Record, { 667 | prefix: uniqueKey, 668 | separator, 669 | }), 670 | ); 671 | else prev.push(`${uniqueKey}=${value}`); 672 | } 673 | 674 | return prev; 675 | }, []) 676 | .join("&"); 677 | } 678 | 679 | function createMuppetEnv( 680 | c: Context, 681 | ) { 682 | return { 683 | ...c.env, 684 | muppet: { 685 | req: c.req, 686 | }, 687 | }; 688 | } 689 | -------------------------------------------------------------------------------- /packages/core/src/openapi.ts: -------------------------------------------------------------------------------- 1 | import type { OpenAPIV3_1 } from "openapi-types"; 2 | import type { MuppetConfiguration, ToolsConfiguration } from "./types"; 3 | import { createMuppetServer, generateKey, mergeSchemas } from "./muppet"; 4 | import { Hono } from "hono"; 5 | import { proxy } from "hono/proxy"; 6 | import type { JSONSchema7 } from "json-schema"; 7 | 8 | export function fromOpenAPI(specs: OpenAPIV3_1.Document) { 9 | const config: MuppetConfiguration = { 10 | name: specs.info.title, 11 | version: specs.info.version, 12 | }; 13 | 14 | const tools: ToolsConfiguration = {}; 15 | 16 | if (specs.paths) 17 | for (const [path, payload] of Object.entries(specs.paths)) { 18 | if (!payload) continue; 19 | 20 | for (const [method, operation] of Object.entries(payload)) { 21 | const schema: Record = {}; 22 | 23 | if ( 24 | typeof operation === "object" && 25 | "parameters" in operation && 26 | operation.parameters 27 | ) { 28 | for (const parameter of operation.parameters) { 29 | if ("$ref" in parameter) { 30 | } else if ("schema" in parameter) { 31 | if (!parameter.schema) continue; 32 | 33 | let pSchema = parameter.schema; 34 | 35 | if ("$ref" in parameter.schema) { 36 | const path = parameter.schema.$ref 37 | .split("/") 38 | .slice(1) 39 | .join("."); 40 | const _pSchema = getPropByPath(specs, path); 41 | 42 | if (_pSchema) { 43 | pSchema = _pSchema; 44 | } 45 | } 46 | 47 | schema[parameter.in] = { 48 | type: "object", 49 | // @ts-expect-error 50 | properties: { 51 | ...(schema[parameter.in]?.properties ?? {}), 52 | [parameter.name]: pSchema, 53 | }, 54 | // @ts-expect-error 55 | required: [ 56 | ...(schema[parameter.in]?.required || []), 57 | parameter.required ? parameter.name : undefined, 58 | ].filter(Boolean), 59 | additionalProperties: false, 60 | }; 61 | } 62 | } 63 | } 64 | 65 | const key = 66 | (typeof operation === "object" && "operationId" in operation 67 | ? operation.operationId 68 | : undefined) ?? generateKey(method, path); 69 | 70 | tools[key] = { 71 | name: key, 72 | description: 73 | typeof operation === "object" && "description" in operation 74 | ? operation.description 75 | : undefined, 76 | path, 77 | method, 78 | inputSchema: mergeSchemas(schema) ?? {}, 79 | schema, 80 | }; 81 | } 82 | } 83 | 84 | const originServer = specs.servers?.[0]?.url || "http://localhost:3000"; 85 | const app = new Hono().all("/:path", async (c) => { 86 | return proxy(`${originServer}/${c.req.param("path")}`, c.req); 87 | }); 88 | 89 | return createMuppetServer({ 90 | config, 91 | specs: { tools }, 92 | app, 93 | }); 94 | } 95 | 96 | const getPropByPath = ( 97 | object: Record, 98 | path: string | string[], 99 | defaultValue?: T, 100 | ): T | undefined => { 101 | const _path = Array.isArray(path) ? path : path.split("."); 102 | 103 | const _key = _path.shift(); 104 | if (object && _key) 105 | return getPropByPath( 106 | object[_key] as Record, 107 | _path, 108 | defaultValue, 109 | ); 110 | return object === undefined ? defaultValue : (object as T); 111 | }; 112 | -------------------------------------------------------------------------------- /packages/core/src/resources.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Env, Handler, Input } from "hono"; 2 | import type { BlankEnv, BlankInput, Next, TypedResponse } from "hono/types"; 3 | import { McpPrimitives, uniqueSymbol } from "./utils"; 4 | import type { Promisify, Resource } from "./types"; 5 | import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status"; 6 | import type { 7 | JSONValue, 8 | SimplifyDeepArray, 9 | InvalidJSONValue, 10 | JSONParsed, 11 | } from "hono/utils/types"; 12 | 13 | type JSONRespondReturn< 14 | T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, 15 | U extends StatusCode, 16 | > = Response & 17 | TypedResponse< 18 | SimplifyDeepArray extends JSONValue 19 | ? JSONValue extends SimplifyDeepArray 20 | ? never 21 | : JSONParsed 22 | : never, 23 | U, 24 | "json" 25 | >; 26 | 27 | export function registerResources< 28 | E extends Env = BlankEnv, 29 | P extends string = string, 30 | I extends Input = BlankInput, 31 | >( 32 | handler: (c: Context, next: Next) => Promisify, 33 | ): Handler> { 34 | // @ts-expect-error 35 | return Object.assign(handler, { 36 | [uniqueSymbol]: { 37 | type: McpPrimitives.RESOURCES, 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/streaming.ts: -------------------------------------------------------------------------------- 1 | import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; 2 | import { 3 | type JSONRPCMessage, 4 | JSONRPCMessageSchema, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | import type { Context } from "hono"; 7 | import { SSEStreamingApi } from "hono/streaming"; 8 | import { getRuntimeKey } from "hono/adapter"; 9 | 10 | /** 11 | * Hono transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. 12 | * 13 | * This can work in all the environments that support SSE with Hono. 14 | */ 15 | export class SSEHonoTransport implements Transport { 16 | private stream!: SSEStreamingApi; 17 | private _stream?: SSEStreamingApi; 18 | private _sessionId: string; 19 | 20 | onclose?: () => void; 21 | onerror?: (error: Error) => void; 22 | onmessage?: (message: JSONRPCMessage) => void; 23 | 24 | /** 25 | * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. 26 | */ 27 | constructor( 28 | private _endpoint: string, 29 | sessionId?: string, 30 | ) { 31 | this._sessionId = sessionId ?? crypto.randomUUID(); 32 | } 33 | 34 | connectWithStream(stream: SSEStreamingApi) { 35 | this.stream = stream; 36 | } 37 | 38 | /** 39 | * Handles the initial SSE connection request. 40 | * 41 | * This should be called when a GET request is made to establish the SSE stream. 42 | */ 43 | async start(): Promise { 44 | if (this._stream) { 45 | throw new Error( 46 | "SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.", 47 | ); 48 | } 49 | 50 | // Send the endpoint event 51 | await this.stream.writeSSE({ 52 | data: `${encodeURI(this._endpoint)}?sessionId=${this._sessionId}`, 53 | event: "endpoint", 54 | }); 55 | 56 | this._stream = this.stream; 57 | 58 | // TODO: Create an issue for this in hono 59 | // this._stream.on("close", () => { 60 | // this._sseResponse = undefined; 61 | // this.onclose?.(); 62 | // }); 63 | } 64 | 65 | /** 66 | * Handles incoming POST messages. 67 | * 68 | * This should be called when a POST request is made to send a message to the server. 69 | */ 70 | async handlePostMessage(c: Context, parsedBody?: unknown): Promise { 71 | if (!this._stream) { 72 | throw new Error("SSE connection not established"); 73 | } 74 | 75 | const body = parsedBody ?? (await c.req.json()); 76 | await this.handleMessage( 77 | typeof body === "string" ? JSON.parse(body) : body, 78 | ); 79 | } 80 | 81 | /** 82 | * Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST. 83 | */ 84 | async handleMessage(message: unknown): Promise { 85 | let parsedMessage: JSONRPCMessage; 86 | try { 87 | parsedMessage = JSONRPCMessageSchema.parse(message); 88 | } catch (error) { 89 | this.onerror?.(error as Error); 90 | throw error; 91 | } 92 | 93 | this.onmessage?.(parsedMessage); 94 | } 95 | 96 | async close(): Promise { 97 | this._stream?.close(); 98 | this._stream = undefined; 99 | this.onclose?.(); 100 | } 101 | 102 | async send(message: JSONRPCMessage): Promise { 103 | if (!this._stream) { 104 | throw new Error("Not connected"); 105 | } 106 | 107 | this._stream.writeSSE({ 108 | data: JSON.stringify(message), 109 | event: "message", 110 | }); 111 | } 112 | 113 | /** 114 | * Returns the session ID for this transport. 115 | * 116 | * This can be used to route incoming POST requests. 117 | */ 118 | get sessionId(): string { 119 | return this._sessionId; 120 | } 121 | } 122 | 123 | const run = async ( 124 | stream: SSEStreamingApi, 125 | cb: (stream: SSEStreamingApi) => Promise, 126 | onError?: (e: Error, stream: SSEStreamingApi) => Promise, 127 | ): Promise => { 128 | try { 129 | await cb(stream); 130 | } catch (e) { 131 | if (e instanceof Error && onError) { 132 | await onError(e, stream); 133 | 134 | await stream.writeSSE({ 135 | event: "error", 136 | data: e.message, 137 | }); 138 | } else { 139 | console.error(e); 140 | } 141 | } 142 | }; 143 | 144 | const contextStash: WeakMap = new WeakMap< 145 | ReadableStream, 146 | Context 147 | >(); 148 | 149 | export const streamSSE = ( 150 | c: Context, 151 | cb: (stream: SSEStreamingApi) => Promise, 152 | onError?: (e: Error, stream: SSEStreamingApi) => Promise, 153 | ): Response => { 154 | const { readable, writable } = new TransformStream(); 155 | const stream = new SSEStreamingApi(writable, readable); 156 | 157 | // Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve() 158 | // if (isOldBunVersion()) { 159 | // c.req.raw.signal.addEventListener('abort', () => { 160 | // if (!stream.closed) { 161 | // stream.abort() 162 | // } 163 | // }) 164 | // } 165 | 166 | // in bun, `c` is destroyed when the request is returned, so hold it until the end of streaming 167 | contextStash.set(stream.responseReadable, c); 168 | 169 | c.header("Transfer-Encoding", "chunked"); 170 | c.header("Content-Type", "text/event-stream"); 171 | c.header("Cache-Control", "no-cache"); 172 | c.header("Connection", "keep-alive"); 173 | 174 | const runtime = getRuntimeKey(); 175 | 176 | if (runtime === "workerd") { 177 | c.header("Content-Encoding", "Identity"); 178 | } 179 | 180 | run(stream, cb, onError); 181 | 182 | return c.newResponse(stream.responseReadable); 183 | }; 184 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter } from "@hono/event-emitter"; 2 | import type { JSONSchema7 } from "json-schema"; 3 | import type { Logger } from "pino"; 4 | import type { McpPrimitivesValue } from "./utils"; 5 | import type { z } from "zod"; 6 | import type { 7 | EmbeddedResourceSchema, 8 | ImageContentSchema, 9 | TextContentSchema, 10 | } from "@modelcontextprotocol/sdk/types.js"; 11 | import type { Env, Hono, HonoRequest, Schema, ValidationTargets } from "hono"; 12 | import type { BlankEnv, BlankInput, BlankSchema, Input } from "hono/types"; 13 | 14 | export type HasUndefined = undefined extends T ? true : false; 15 | 16 | export type DescribeOptions = { 17 | name?: string; 18 | description?: string; 19 | }; 20 | 21 | export type PromptDescribeOptions = DescribeOptions & { 22 | completion?: CompletionFn; 23 | }; 24 | 25 | export type ToolDescribeOptions = DescribeOptions & { 26 | resourceType?: "raw" | "text"; 27 | }; 28 | 29 | export type CompletionFn = (args: { 30 | name: string; 31 | value: string; 32 | }) => Promisify< 33 | | { 34 | /** 35 | * An array of completion values. Must not exceed 100 items. 36 | */ 37 | values: string[]; 38 | /** 39 | * The total number of completion options available. This can exceed the number of values actually sent in the response. 40 | */ 41 | total?: number; 42 | /** 43 | * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. 44 | */ 45 | hasMore?: boolean; 46 | } 47 | | string[] 48 | >; 49 | 50 | export type ToolHandlerResponse = { 51 | toJson?: 52 | | DescribeOptions 53 | | ((config?: Record) => Promise); 54 | /** 55 | * Target for validation 56 | */ 57 | validationTarget?: keyof ValidationTargets; 58 | type?: McpPrimitivesValue; 59 | }; 60 | 61 | export type BaseEnv< 62 | E extends Env = BlankEnv, 63 | S extends Schema = BlankSchema, 64 | P extends string = string, 65 | > = { 66 | // biome-ignore lint/complexity/noBannedTypes: Need to pass env to Hono 67 | Bindings: {}; 68 | Variables: { 69 | muppet: MuppetConfiguration; 70 | specs: ServerConfiguration; 71 | app: Hono; 72 | }; 73 | }; 74 | 75 | export type CreateMuppetOptions< 76 | E extends Env = BlankEnv, 77 | S extends Schema = BlankSchema, 78 | P extends string = string, 79 | > = { 80 | specs: ServerConfiguration; 81 | config: MuppetConfiguration; 82 | app: Hono; 83 | }; 84 | 85 | export type ServerConfiguration = { 86 | tools?: ToolsConfiguration; 87 | prompts?: PromptConfiguration; 88 | resources?: ResourceConfiguration; 89 | }; 90 | 91 | export type ToolsConfiguration = Record< 92 | string, 93 | ToolDescribeOptions & { 94 | inputSchema: JSONSchema7; 95 | schema?: { [K in keyof ValidationTargets]?: JSONSchema7 }; 96 | path: string; 97 | method: string; 98 | } 99 | >; 100 | 101 | export type PromptConfiguration = Record< 102 | string, 103 | PromptDescribeOptions & { 104 | arguments: { name: string; description?: string; required?: boolean }[]; 105 | schema?: { [K in keyof ValidationTargets]?: JSONSchema7 }; 106 | path: string; 107 | method: string; 108 | } 109 | >; 110 | 111 | export type ResourceConfiguration = Record< 112 | string, 113 | { 114 | path: string; 115 | method: string; 116 | } 117 | >; 118 | 119 | export type Resource = 120 | | { 121 | type?: "direct"; 122 | uri: string; 123 | name: string; 124 | description?: string; 125 | mimeType?: string; 126 | } 127 | | { 128 | type: "template"; 129 | uri: string; 130 | name: string; 131 | description?: string; 132 | mimeType?: string; 133 | completion?: CompletionFn; 134 | }; 135 | 136 | export type ResourceResponse = { 137 | uri: string; 138 | mimeType?: string; 139 | /** 140 | * For text resources 141 | */ 142 | text?: string; 143 | /** 144 | * For binary resources (base64 encoded) 145 | */ 146 | blob?: string; 147 | }; 148 | 149 | // Path -> Method -> Configuration 150 | export type ConceptConfiguration = Record< 151 | string, 152 | | Record< 153 | string, 154 | | (DescribeOptions & { 155 | schema?: { [K in keyof ValidationTargets]?: JSONSchema7 }; 156 | resourceType?: "raw" | "text"; 157 | type?: McpPrimitivesValue; 158 | completion?: CompletionFn; 159 | }) 160 | | undefined 161 | > 162 | | undefined 163 | >; 164 | 165 | export type ClientToServerNotifications = { 166 | "notifications/initialized": undefined; 167 | "notifications/cancelled": undefined; 168 | "notifications/roots/list_changed": undefined; 169 | }; 170 | 171 | export type ServerToClientNotifications = { 172 | "notifications/cancelled": undefined; 173 | "notifications/message": undefined; 174 | "notifications/tools/list_changed": undefined; 175 | "notifications/prompts/list_changed": undefined; 176 | "notifications/resources/updated": undefined; 177 | "notifications/resources/list_changed": undefined; 178 | "notifications/progress": undefined; 179 | }; 180 | 181 | export type SubscriptionEvents = { 182 | "resources/unsubscribe": undefined; 183 | "resources/subscribe": undefined; 184 | }; 185 | 186 | export type Promisify = T | Promise; 187 | 188 | export type ResourceFetcherFn = ( 189 | uri: string, 190 | ) => Promisify; 191 | 192 | export type MuppetConfiguration = { 193 | name: string; 194 | version: string; 195 | logger?: Logger; 196 | events?: Emitter; 197 | resources?: Record; 198 | symbols?: unknown[]; 199 | }; 200 | 201 | export type ToolContentResponseType = 202 | | z.infer 203 | | z.infer 204 | | z.infer; 205 | 206 | export type ToolResponseType = 207 | | { content: ToolContentResponseType[] } 208 | | ToolContentResponseType[]; 209 | 210 | export type PromptContentResponseType = { 211 | role: "user" | "assistant"; 212 | content: ToolContentResponseType; 213 | }; 214 | 215 | export type PromptResponseType = 216 | | { 217 | description: string; 218 | messages: PromptContentResponseType[]; 219 | } 220 | | PromptContentResponseType[]; 221 | 222 | export type MuppetEnv< 223 | P extends string = string, 224 | I extends Input = BlankInput, 225 | > = { 226 | req: HonoRequest; 227 | }; 228 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The unique symbol for the middlewares, which makes it easier to identify them. Not meant to be used directly, unless you're creating a custom middleware. 3 | */ 4 | export const uniqueSymbol = Symbol("muppet"); 5 | 6 | export const McpPrimitives = { 7 | RESOURCES: "resources", 8 | TOOLS: "tools", 9 | PROMPTS: "prompts", 10 | } as const; 11 | 12 | export type McpPrimitivesValue = 13 | (typeof McpPrimitives)[keyof typeof McpPrimitives]; 14 | -------------------------------------------------------------------------------- /packages/core/src/validator.ts: -------------------------------------------------------------------------------- 1 | import { type Hook, sValidator } from "@hono/standard-validator"; 2 | import { toJsonSchema } from "@standard-community/standard-json"; 3 | import type { StandardSchemaV1 } from "@standard-schema/spec"; 4 | import type { Env, Input, MiddlewareHandler, ValidationTargets } from "hono"; 5 | import type { HasUndefined } from "./types.js"; 6 | import { uniqueSymbol } from "./utils.js"; 7 | 8 | /** 9 | * Create a validator middleware. Use this only for muppet tools. 10 | * @param target Target for validation 11 | * @param schema Standard Schema 12 | * @param hook Hook for validation 13 | * @returns Middleware handler 14 | */ 15 | export function mValidator< 16 | Schema extends StandardSchemaV1, 17 | Target extends keyof ValidationTargets, 18 | E extends Env, 19 | P extends string, 20 | In = StandardSchemaV1.InferInput, 21 | Out = StandardSchemaV1.InferOutput, 22 | I extends Input = { 23 | in: HasUndefined extends true 24 | ? { 25 | [K in Target]?: In extends ValidationTargets[K] 26 | ? In 27 | : { [K2 in keyof In]?: ValidationTargets[K][K2] }; 28 | } 29 | : { 30 | [K in Target]: In extends ValidationTargets[K] 31 | ? In 32 | : { [K2 in keyof In]: ValidationTargets[K][K2] }; 33 | }; 34 | out: { [K in Target]: Out }; 35 | }, 36 | V extends I = I, 37 | >( 38 | target: Target, 39 | schema: Schema, 40 | hook?: Hook, E, P, Target>, 41 | ): MiddlewareHandler { 42 | const middleware = sValidator(target, schema, hook); 43 | 44 | // @ts-expect-error not typed well 45 | return Object.assign(middleware, { 46 | [uniqueSymbol]: { 47 | validationTarget: target, 48 | toJson: async () => ({ 49 | schema: await toJsonSchema(schema), 50 | }), 51 | }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "useDefineForClassFields": false, 7 | "experimentalDecorators": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noImplicitAny": true, 18 | "noUnusedParameters": true, 19 | "declaration": false, 20 | "noEmit": true, 21 | "outDir": "dist/", 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true, 28 | "baseUrl": ".", 29 | "paths": { 30 | "@/*": ["./src/*"] 31 | } 32 | }, 33 | "include": ["./src/**/*"] 34 | } 35 | -------------------------------------------------------------------------------- /packages/create-muppet/README.md: -------------------------------------------------------------------------------- 1 | # Create Muppet 2 | 3 | Create a Muppet application from starter templates. 4 | -------------------------------------------------------------------------------- /packages/create-muppet/build.ts: -------------------------------------------------------------------------------- 1 | import { build } from "esbuild"; 2 | 3 | build({ 4 | bundle: true, 5 | entryPoints: ["./src/index.ts"], 6 | banner: { 7 | js: "#!/usr/bin/env node", 8 | }, 9 | platform: "node", 10 | outfile: "bin", 11 | format: "cjs", 12 | minify: true, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/create-muppet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-muppet", 3 | "description": "Create a Muppet application from starter templates.", 4 | "version": "0.1.3", 5 | "license": "MIT", 6 | "bin": "./bin", 7 | "files": [ 8 | "bin" 9 | ], 10 | "scripts": { 11 | "bin": "./bin", 12 | "build": "tsx ./build.ts" 13 | }, 14 | "keywords": [ 15 | "mcps", 16 | "mcp", 17 | "create", 18 | "muppet", 19 | "template" 20 | ], 21 | "homepage": "https://github.com/muppet-dev/muppet", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/muppet-dev/muppet.git", 28 | "directory": "packages/create-muppet" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/muppet-dev/muppet/issues" 32 | }, 33 | "devDependencies": { 34 | "@clack/prompts": "^0.10.0", 35 | "@types/node": "^22.13.15", 36 | "chalk": "^5.4.1", 37 | "commander": "^13.1.0", 38 | "esbuild": "^0.25.2", 39 | "execa": "^9.5.2", 40 | "giget": "1.2.3", 41 | "tsx": "^4.19.3" 42 | } 43 | } -------------------------------------------------------------------------------- /packages/create-muppet/src/cleanup.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | const PROJECT_NAME = new RegExp(/%%PROJECT_NAME.*%%/g); 5 | const WRANGLER_FILES = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"]; 6 | const COMPATIBILITY_DATE_TOML = /compatibility_date\s*=\s*"\d{4}-\d{2}-\d{2}"/; 7 | const COMPATIBILITY_DATE_JSON = 8 | /"compatibility_date"\s*:\s*"\d{4}-\d{2}-\d{2}"/; 9 | 10 | type CleanupOptions = { 11 | dir: string; 12 | name: string; 13 | runtime: string; 14 | }; 15 | 16 | export function cleanup(options: CleanupOptions) { 17 | const { dir, name, runtime } = options; 18 | 19 | if (runtime === "cloudflare-workers") { 20 | for (const filename of WRANGLER_FILES) { 21 | try { 22 | const wranglerPath = path.join(dir, filename); 23 | const wrangler = fs.readFileSync(wranglerPath, "utf-8"); 24 | // Get current date in YYYY-MM-DD format 25 | const currentDate = new Date().toISOString().split("T")[0]; 26 | const convertProjectName = name 27 | .toLowerCase() 28 | .replaceAll(/[^a-z0-9\-_]/gm, "-"); 29 | const rewritten = wrangler 30 | .replaceAll(PROJECT_NAME, convertProjectName) 31 | .replace( 32 | COMPATIBILITY_DATE_TOML, 33 | `compatibility_date = "${currentDate}"`, 34 | ) 35 | .replace( 36 | COMPATIBILITY_DATE_JSON, 37 | `"compatibility_date": "${currentDate}"`, 38 | ); 39 | fs.writeFileSync(wranglerPath, rewritten); 40 | } catch {} 41 | } 42 | } 43 | 44 | const packageJsonPath = path.join(dir, "package.json"); 45 | 46 | if (fs.existsSync(packageJsonPath)) { 47 | const packageJson = fs.readFileSync(packageJsonPath, "utf-8"); 48 | 49 | const packageJsonParsed = JSON.parse(packageJson); 50 | 51 | packageJsonParsed.name = name; 52 | packageJsonParsed.nx = undefined; 53 | packageJsonParsed.dependencies.muppet = "latest"; 54 | 55 | fs.writeFileSync( 56 | packageJsonPath, 57 | JSON.stringify(packageJsonParsed, null, 2), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/create-muppet/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as p from "@clack/prompts"; 2 | import chalk from "chalk"; 3 | import { type Command, Option, program } from "commander"; 4 | import { exec } from "node:child_process"; 5 | import fs from "node:fs"; 6 | import path from "node:path"; 7 | import { chdir, exit } from "node:process"; 8 | import pkg from "../package.json" assert { type: "json" }; 9 | import { cleanup } from "./cleanup"; 10 | import { figureOutPackageManager } from "./package-manager"; 11 | import { 12 | ALL_TRANSPORT_LAYERS, 13 | ALL_UNIQUE_TEMPLATES, 14 | RUNTIMES_BY_TRANSPORT_LAYER, 15 | TRANSPORT_LAYERS, 16 | download, 17 | } from "./template"; 18 | import { 19 | type PackageManager, 20 | knownPackageManagerNames, 21 | knownPackageManagers, 22 | } from "./utils"; 23 | 24 | const IS_CURRENT_DIR_REGEX = /^(\.\/|\.\\|\.)$/; 25 | 26 | function mkdirp(dir: string) { 27 | try { 28 | fs.mkdirSync(dir, { recursive: true }); 29 | } catch (e) { 30 | if (e instanceof Error) { 31 | if ("code" in e && e.code === "EEXIST") return; 32 | } 33 | throw e; 34 | } 35 | } 36 | 37 | program 38 | .name("create-muppet") 39 | .description("Create a new Muppet project") 40 | .version(pkg.version) 41 | .arguments("[target]") 42 | .addOption(new Option("-i, --install", "Install dependencies")) 43 | .addOption( 44 | new Option("-p, --pm ", "Package manager to use").choices( 45 | knownPackageManagerNames, 46 | ), 47 | ) 48 | .addOption( 49 | new Option("-t, --transport ", "Transport to use").choices( 50 | ALL_TRANSPORT_LAYERS, 51 | ), 52 | ) 53 | .addOption( 54 | new Option("-r, --runtime