Skip to main content
Glama

mcpkit

Many MCP servers in one binary. Each server is a JSON manifest that describes its API, auth, tools and response mapping. A generic executor runs those manifests.

Repository layout

packages/core      @usemcpkit/core      library: manifest format, SDK, executor, auth (publishable)
packages/ui        @usemcpkit/ui        Svelte pages the CLI opens in the browser, e.g. the credentials form (private)
packages/testing   @usemcpkit/testing   shared test fixtures (private)
apps/cli           @usemcpkit/cli       mcpkit CLI + MCP stdio server, server definitions, binaries
apps/npx           usemcpkit         wrapper so `npx usemcpkit` runs @usemcpkit/cli

Yarn 4 workspaces and Nx, Node 22.18+. Every command below runs from the repository root.

yarn install
yarn build          # every package; apps/cli/build/mcpkit.cli.js + packages/servers/build/manifests/
yarn test
yarn typecheck
yarn lint           # Biome: lint + format + import order check per project
yarn format         # Biome: apply formatting and safe fixes across the repo
yarn package        # + binaries in apps/cli/build/bin and npm packages in apps/cli/build/npm (needs bun)
MCPKIT_TARGETS=darwin-arm64,linux-x64 yarn package   # only some targets

Every package builds from src/main.ts into build/mcpkit.<package>.js (mcpkit.core.d.ts for the published types, mcpkit.ui.html for the UI page). Nx runs lint, typecheck and test before every build, and builds dependencies first.

Related MCP server: Git MCP Server

Install

npx usemcpkit

This finds the AI clients installed for the current user, asks which ones to set up, copies the mcpkit binary for this OS to ~/.mcpkit/bin/mcpkit and adds one mcpkit entry (<binary> serve, every server) to each chosen client. Running it again updates the entries in place. Only the mcpkit key is touched: comments and other keys stay, and the previous file is kept as <config>.mcpkit-backup. Restart the clients afterwards (later updates reload live).

npx usemcpkit --all                         # every detected client, no checklist (needed without a terminal)
npx usemcpkit --clients cursor,codex        # only these
mcpkit install --binary ./mcpkit         # install a local binary instead of downloading one
mcpkit uninstall [--all | --clients ...] # remove the entry; the binary stays
mcpkit upgrade                           # update the installed binary now

Supported clients (ids): claude-desktop, claude-code (~/.claude.json), cursor, vscode, vscode-insiders (servers in the user mcp.json), devin-desktop (Windsurf's new home), windsurf (legacy ~/.codeium/windsurf), cline (~/.cline), roo-code, copilot-cli, codex (TOML), gemini-cli, zed (context_servers), opencode (mcp), kiro, lm-studio, junie, warp. A client counts as installed when its config directory exists. Each client's path and entry shape lives in packages/clients/src/definitions/<id>.ts.

Where the binary comes from. When the CLI runs as the compiled binary it copies itself. Under Node (npx) it downloads @usemcpkit/cli-<target> (for example @usemcpkit/cli-darwin-arm64, @usemcpkit/cli-linux-x64-musl) at the same version from the npm registry and checks the tarball against its published sha512 integrity. yarn package writes those packages to apps/cli/build/npm/cli-<target>/; publish each of them together with @usemcpkit/cli and mcpkit.

Auto-update. mcpkit serve, when it is the installed binary, checks the npm registry at most once a day (~/.mcpkit/update-state.json) in the background. A newer release replaces the binary and is picked up without restarting the client, see Live reload. MCPKIT_AUTO_UPDATE=0 turns this off, MCPKIT_NPM_REGISTRY points at another registry.

Live reload. A compiled mcpkit serve is a small supervisor: it keeps the client's stdio connection and runs the real server as a worker (mcpkit serve --worker). It polls its own binary file and, once the file was replaced and stayed the same for two polls (3 s each), starts the new binary as a second worker, replays the client's initialize handshake to it and only then switches new requests over. The old worker finishes the requests it already has (up to 5 minutes) and exits. The client is told with notifications/tools/list_changed, which the server advertises as tools.listChanged, so new and changed tools show up without a restart. The same applies when you copy a rebuilt binary over ~/.mcpkit/bin/mcpkit or when auto-update downloads a release. If the new binary fails to answer initialize within 15 s, the running server stays and the reason is logged to stderr. The supervisor itself is not swapped: a release that changes it needs one client restart. Clients that ignore list_changed need a restart to see changed tool lists; calls always run on the new server. A Homebrew upgrade moves the binary to a new Cellar/<version> folder, so that install is not reloaded live.

Homebrew and winget. The package only installs the binary. Running mcpkit afterwards shows the same client checklist. A binary under Homebrew's Cellar/mcpkit/<version> is used in place: clients get <prefix>/bin/mcpkit, the link Homebrew moves on every upgrade. A winget binary under WinGet\Packages\<id>_… is also used in place. Neither is copied to ~/.mcpkit/bin or updated by mcpkit; mcpkit upgrade prints the brew upgrade / winget upgrade command instead. The Homebrew formula's caveats and the winget manifest's InstallationNotes should say "Run mcpkit to set up your AI clients."

CLI

packages/servers/src/servers/gitlab/gitlab.ts   author servers in TypeScript (one folder per server, one tool per file)
  └─ vite build                          packages/servers: plugins/manifests/ compiles + validates src/servers/*, then:
       ├─ build/mcpkit.cli.js             the CLI, no manifests inside
       └─ build/manifests/*.json          compiled manifests + index.json (the registry; CI publishes it to GitHub Pages)
            └─ yarn package              plugins/binaries/ runs bun compile → build/bin/mcpkit-<os>-<arch>

An invalid server definition fails the build. The build log lists each compiled manifest:

mcpkit: 1 manifest(s) → build/manifests
  github  1.0.0  1239 tools  bearer GITHUB_TOKEN  1.8 MB

Adding a server

Create packages/servers/src/<name>/<name>.ts with exactly one named export built with defineServer from @usemcpkit/core. Folder dashes become underscores in the server name (my-api → my_api). Put each tool in packages/servers/src/<name>/tools/<resource>/<action>.ts (the tool pull_request:list lives in tools/pull-request/list.ts) and shared params in const.ts.

Use with an MCP client

npx usemcpkit writes the entry for you (see Install). By hand:

{
  "mcpServers": {
    "mcpkit": {
      "command": "/Users/me/.mcpkit/bin/mcpkit",
      "args": ["serve"]
    }
  }
}

mcpkit serve with no names serves every server; mcpkit serve github only some. Tools are named <server>_<tool>.

Bundled servers

Server

Category

Hosts

Tools

github

git

github.com, GitHub Enterprise Server, GHE.com

the whole GitHub REST API (1,239 operations), tools named after its operation ids, for example pulls:list, issues:create, repos:get_content; generated by yarn workspace @usemcpkit/servers generate:github

gitlab

git

gitlab.com, self-managed GitLab (<host>[/<relative url root>])

the GitLab REST API (1,648 operations), tools named <group>:<resource>:<action> after the API tag and path, for example projects:projects:list_projects_issues, issues:notes:create_projects_merge_requests_notes; generated by yarn workspace @usemcpkit/servers generate:gitlab

jira

atlassian

Jira Cloud (<site>.atlassian.net), Jira Data Center

the Jira platform REST API (/rest/api/2, 625 tools with Jira Software boards and sprints under software:), named <group>:<resource>:<action>, for example issues:issues:create_issue, issues:issue_search:search_and_reconsile_issues_using_jql (picks the Cloud or Data Center endpoint), software:board:get_all_boards; generated by yarn workspace @usemcpkit/servers generate:jira

confluence

atlassian

Confluence Cloud (<site>.atlassian.net), Confluence Data Center

the Confluence REST APIs (529 operations) in three groups: cloud (Cloud REST v2, for example cloud:pages:create_page), cloud_v1 (Cloud REST v1, cloud_v1:search:search_by_cql) and datacenter (Data Center 10.2 REST, datacenter:content:create); generated by yarn workspace @usemcpkit/servers generate:confluence

clickup

productivity

ClickUp (api.clickup.com)

the ClickUp REST API v2 and v3 (171 operations) in eight groups (tasks, hierarchy, time, goals, users, chat, docs, admin), named <group>:<resource>:<action> after the operation summary, for example tasks:tasks:get_tasks, hierarchy:lists:create_list, docs:docs:search_for_docs; generated by yarn workspace @usemcpkit/servers generate clickup

discord

communication

Discord (discord.com/api/v10)

the Discord HTTP API v10 as a bot (241 operations) in eight groups (channels, guilds, users, applications, webhooks, invites, lobbies, global), named <group>:<resource>:<action> after the operation id, for example channels:messages:create_message, guilds:members:list_guild_members, users:guilds:list_my_guilds; generated by yarn workspace @usemcpkit/servers generate discord

GitLab reads GITLAB_HOST (default gitlab.com) and a personal access token in GITLAB_TOKEN. Tools take a project id or its full path (group/subgroup/project) as id, and issue and merge request numbers are the per-project iid.

Jira and Confluence ask for the host first (JIRA_HOST / CONFLUENCE_HOST, for example acme.atlassian.net or jira.example.com/jira), then for one of two options: Cloud: email + API token (JIRA_USERNAME + JIRA_API_TOKEN, CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN) or Data Center: personal access token (JIRA_PERSONAL_TOKEN, CONFLUENCE_PERSONAL_TOKEN). Cloud and Data Center use different endpoints where Atlassian removed the old ones on Cloud (Jira issue search); the host picks them, see Editions. Confluence names the deployment in the tool group instead: call cloud or cloud_v1 tools on a *.atlassian.net host and datacenter tools on Data Center. On Cloud, search_issues returns the first max_results issues only (start_at is Data Center only), and Confluence Cloud v2 tools take a space id (cloud:spaces:get_spaces) instead of a space key.

ClickUp reads a personal API token (pk_…, from Settings → Apps) in CLICKUP_API_TOKEN and sends it as the Authorization header. The v2 tools call the Workspace id team_id (hierarchy:workspaces:get_authorized_workspaces lists them), and array filters such as statuses take a list.

Discord reads a bot token (Developer Portal → your application → Bot) in DISCORD_BOT_TOKEN and sends it as Authorization: Bot <token>; the bot must be in a guild to reach its channels. Ids are snowflakes passed as strings. The five endpoints that accept only a user's OAuth2 token are left out. To read message text, turn on the privileged Message Content intent on the Bot page; without it Discord returns other users' guild messages with an empty content.

Manifest format

{
  "name": "github",                       // [a-z][a-z0-9_]*
  "category": "git",                      // [a-z][a-z0-9-]*, groups servers of one kind
  "version": "1.0.0",
  "baseUrl": "https://{domain}",          // https only; {domain} is optional, see Domain
  "domain": { "label": "GitHub API host", "default": "api.github.com", "env": "GITHUB_API_HOST" },
  "headers": { "Accept": "application/vnd.github+json" },
  "auth": [                               // tried top to bottom, see Auth
    { "type": "bearer", "env": "GITHUB_TOKEN" }
  ],
  "tools": [
    {
      "name": "list_issues",
      "description": "List issues in a repository.",
      "input": { "type": "object", "properties": { ... }, "required": ["owner", "repo"] },
      "request": {
        "method": "GET",
        "path": "/repos/{owner}/{repo}/issues",  // {arg} → URL-encoded argument
        "query": ["state", "per_page"],          // or { "q": "query" } to rename
        "body": ["title"],                       // JSON body for non-GET, see Request bodies
        "commaQuery": ["tags"],                  // optional: array query keys joined with commas (others repeat the key)
        "fileArgs": ["file"],                    // optional: args holding a local file path; sent as multipart parts (contentType multipart/form-data) or as the raw bodyArg
        "saveToArg": "save_to",                  // optional: arg holding a local path; a successful response is written there and a summary returned
        "fixedQuery": { "sort": "updated" },     // optional: constant query values
        "contentType": "text/plain",             // optional: default application/json; text/* sends bodyArg as plain text
        "fixedHeaders": { "X-Atlassian-Token": "no-check" }, // optional: constant request headers
        "fixedBody": { "type": "issue" },        // optional: constant body values, args win
        "bodyArg": "body"                        // optional: send this arg as the whole JSON body (array, string or free-form object); excludes body/fixedBody
      },
      "response": {
        "root": "items",                         // optional: dig into the payload
        "fields": { "author": "user.login", "labels": "labels[].name" }
      }
    }
  ]
}

Request bodies. Outgoing body keys may be dotted paths that build nested JSON, with numbers as array indexes: { "fields.project.key": "project_key", "ancestors.0.id": "parent_id" } sends {"fields":{"project":{"key":"..."}},"ancestors":[{"id":"..."}]}. fixedBody keys work the same way. Fixed values are strings, numbers or booleans.

Editions. One server can cover deployments whose endpoints differ, such as Atlassian Cloud and Data Center. The manifest names them with the host suffixes that select them; a tool overrides its request (and response, when given) per edition. A domain matching no edition uses the tool's own request:

"editions": { "cloud": { "label": "Cloud", "hosts": [".atlassian.net"] } },   // names: [a-z][a-z0-9-]*
"tools": [
  {
    "name": "search_issues",
    "request": { "method": "GET", "path": "/rest/api/2/search", "query": ["jql"] },
    "editions": {
      "cloud": { "request": { "method": "GET", "path": "/rest/api/2/search/jql", "query": ["jql"] } }
    }
  }
]

The edition follows the domain in use, including one typed in the credentials form.

Auth

auth lists OAuth2 entries (oauth) first, then fallbacks. A server can use fallbacks only:

"auth": [
  { "type": "oauth", "authorizationUrl": "...", "tokenUrl": "...", "clientId": "..." },   // optional
  { "type": "bearer", "env": "GITHUB_TOKEN" }                                            // fallback
]

Every tool call tries, in this order, and uses the first that works:

  1. the server's OAuth2 token in tokens.json, refreshed if it expired (only when auth has oauth entries);

  2. each fallback whose value is set, top to bottom. A value comes from its env var, or else from config.json;

  3. the credentials form: a local page opened in the browser that asks for the missing values (and the domain, see below), or offers "Sign in with OAuth2" when the server has oauth entries.

An option is skipped when its credentials are missing or the API answers 401. A fallback that worked is tried right after tokens.json on later calls in the same process. If nothing works, the call fails with one line per option saying why. none can only be the last entry. Leaving out auth means no auth.

Fallbacks. The manifest never holds a secret, only where to find it: none, bearer {env}, header {env, name, prefix?}, query {env, name}, basic {usernameEnv, passwordEnv} (each also takes an optional label, the option's title in the form; all but none take an optional ui, see below). Env vars always win, so CI keeps working without a browser.

Credentials form. mcpkit serve starts a one-off server on 127.0.0.1 (a free port), prints its URL to stderr and opens the browser. Only the page that was opened can answer: the URL carries a session token in its #fragment and the server checks it and the Host header. Concurrent calls share one form; it closes after an answer, on Cancel, or after 5 minutes. Secrets are saved only after the API accepts them, so a typo is never stored.

Config file. The hosts and the saved credentials live together in ~/.mcpkit/config.json, by category, server and host ({ "servers": { "git": { "gitlab": { "hosts": { "git.acme.com": { "secrets": { "GITLAB_TOKEN": "glpat-…" } } }, "projects": { "/work/app": "git.acme.com" } } } } }), which you can edit by hand. Credentials never cross hosts. With several hosts for one server, the host of a call is, in order: its env var; the host its project (git root) was bound to the first time you answered the form; the saved host the project's origin remote points at (servers whose manifest sets domain.matchGitRemote, such as GitHub and GitLab); otherwise the form asks, and the answer is remembered for that project. Credentials are plain text, with file mode 0600 in a 0700 directory, the way gh and glab keep their tokens. The file carries a version (now 2); a file without one is version 1 and, like an older secrets.json, is upgraded on first use by the migration list in auth/config/config-migrations.ts (a new version is one appended entry), and a file from a newer mcpkit is refused rather than overwritten. There is no password and nothing to unlock. Any program running as your user can read the file, so protect your account and disk; to forget a credential, delete its entry or the file. Environment variables always win over the file.

Form UI. The page holds no text of its own. The CLI injects the whole view model, built from the manifest, into the page as JSON, and the page only renders it. Wording and buttons come from the manifest through defineUI:

import { defineServer, defineUI } from '@usemcpkit/core';

defineServer({
  // ...
  ui: defineUI({ texts: { title: 'Connect {server}', submit: 'Save' } }), // any key of the default texts
  auth: [
    {
      type: 'bearer',
      env: 'GITHUB_TOKEN',
      ui: defineUI({
        inputs: { GITHUB_TOKEN: { label: 'Token', hint: '…', placeholder: 'ghp_…' } },
        actions: [
          { type: 'link', label: 'Create on GitHub ↗', url: 'https://{webHost}/settings/tokens/new?name=mcpkit+{server}' },
          { type: 'cli', label: 'Use GitHub CLI', command: 'gh', args: ['auth', 'token', '--hostname', '{webHost}'] },
        ],
      }),
    },
  ],
});
  • texts (on the manifest) overrides the defaults in core (DEFAULT_UI_TEXTS); {server} is filled in. domain also takes hint and placeholder.

  • inputs (on an auth entry, keyed by env var name) sets a field's label, hint and placeholder.

  • A link action opens an https:// URL. {domain} is the typed domain, {webHost} its web host (api.github.com → github.com, api.<name>.ghe.com → <name>.ghe.com, <host>/api/v3 → <host>), {server} the server name. The link follows the domain field and stays hidden while it needs a domain and there is none.

  • A cli action carries its own command and args; core has no list of tools. command must be a program name found on PATH (no path, no arguments), and args may use {domain} and {webHost}. On click the CLI process, not the page, runs it without a shell, with a 15 s timeout; the token it prints goes straight to the API check and never reaches the browser. It is offered only for an entry with a single credential, and only when the program is on PATH when the form opens. Nothing runs without the click.

GitLab offers Use GitLab CLI, which runs glab config get token --host <host>. Create on GitLab ↗ opens the personal access token page of the server's host (<host>/-/user_settings/personal_access_tokens, under the relative URL root if the domain has one), prefilled with a name and the api scope.

The CLI button is offered only for options with a single field. A domain typed in the form is used once it is valid. An imported token is saved like a typed one: only after the API accepts it. Manifests can be updated remotely (see below), so only use a registry you trust: its cli buttons name the programs mcpkit runs when you click them.

Domain. A server whose host differs per user (GitHub Enterprise, self-hosted GitLab, Jira...) writes {domain} in baseUrl and, if it has them, in the OAuth URLs, and declares domain:

"domain": { "label": "GitHub API host", "default": "api.github.com", "env": "GITHUB_API_HOST" }   // all optional

The domain comes from the env var, else config.json, else default. The form shows it prefilled and lets the user change it (host[:port][/path], no scheme), unless the env var is set.

OAuth 2.0 with PKCE. The user signs in once in the browser against the provider directly:

{
  "type": "oauth",
  "authorizationUrl": "https://example.com/oauth/authorize",   // https only, {domain} allowed
  "tokenUrl": "https://example.com/oauth/token",               // https only, {domain} allowed
  "clientId": "public-client-id",
  "clientSecretEnv": "EXAMPLE_CLIENT_SECRET",                  // optional, for providers that need one
  "scopes": ["read", "write"],                                 // optional
  "redirectPort": 8976                                         // optional, 0 picks a free port
}

The sign-in starts on the first tool call that has no working token: mcpkit serve prints the URL to stderr and opens the browser. The redirect goes to http://127.0.0.1:<port>/callback, and the authorization code is exchanged with a S256 code verifier. The token is saved to ~/.mcpkit/tokens.json (MCPKIT_HOME overrides) with file mode 0600, together with the entry that issued it. Tool calls send it as a Bearer token and refresh it before it expires. Concurrent calls share one sign-in. If the MCP client gives up waiting, the sign-in still finishes and the next call uses the token.

Server manifests

No manifest ships inside the binary. They live in a registry (by default the repository's GitHub Pages, published by .github/workflows/registry.yml whenever packages/servers/src or the manifest compiler changes):

index.json                  every server: id, version, description, link, sha256 (no tools, so it stays small)
<category>/<server>.json    one manifest, for example git/gitlab.json
<category>/<server>/<group>.json  with grouped tool names (git/github.json is a shell listing git/github/<group>.json)

Only index.json is synced by default (into ~/.mcpkit/manifests, MCPKIT_HOME overrides). A manifest is downloaded the first time a tool of that server is described or called, checked against the sha256 in the index, and cached.

  • mcpkit install and the first serve/list with nothing cached download the index.

  • After that serve checks once a day in the background (~/.mcpkit/manifests/.synced); the result applies at the next start. MCPKIT_AUTO_UPDATE=0 turns the background check off.

  • mcpkit update [--registry <url>] syncs now: it refreshes the index and re-downloads cached manifests whose checksum changed, so an edit published without a version bump still lands. The URL comes from the flag, MCPKIT_REGISTRY, then the default.

  • Self-hosting a registry is hosting the packages/servers/build/manifests/ folder (links in index.json are built from the registry URL in apps/cli/vite.config.ts).

What the AI client sees

The tool list starts as one section per served server (git_github, git_gitlab, ...). A section is not a tool: its description says so, it takes no arguments, and calling it opens it. A server split into groups answers with its groups, each one another section (git_github_issues); calling a group, or a server that has no groups, puts its tools into the list and mcpkit sends tools/list_changed. Only what the model opens is downloaded: the index first, then the server's shell, then the one group file. Every real tool is its own MCP tool, with its description and input schema, so the client's permission rules apply per tool: git:github with issues:issues:get_issue is git_github_issues_issues_get_issue (mcp__mcpkit__git_github_issues_issues_get_issue in Claude Code). There are no call_tool or describe_tool meta-tools, and opened sections are forgotten when the server restarts. Tool names cannot contain : and the API caps a name at 64 characters, 13 of which the client spends on mcp__mcpkit__, so a name longer than 51 characters is cut at a word boundary and ends in a 6-character hash of its full id (..._comments_create_d9463b). The hash depends only on the id, so a name and the permission rule written for it stay the same from run to run.

Opening a section downloads its file once (cached under ~/.mcpkit/manifests). A section that cannot be opened stays closed and the call returns an error saying why. Reads are marked read-only, deletes destructive and known writes as writes; any other verb is left unannotated.

Ids still come from index.json: <category>:<server>:<resource>:<action> selects <category>/<server>.json. mcpkit serve [servers…] limits the catalog to the given ids or bare names.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A unified MCP server with composable tools for GitHub operations, file management, shell execution, kanban boards, Discord messaging, and package management. Features role-based security, HTTP/stdio transports, and a web-based development UI.
    GPL 3.0
  • A
    license
    B
    quality
    D
    maintenance
    A modular MCP server that provides a unified interface for interacting with GitHub and GitLab, including enterprise and self-hosted instances. It enables comprehensive management of repositories, issues, pull requests, and CI/CD pipelines through natural language.
    23
    133 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that enables interaction with Jira to fetch issues by key and perform JQL searches. It provides a foundation for integrating multiple work systems, with planned support for Slack and GitHub.
    655 npm
    MIT