Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions bun.lock

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

20 changes: 20 additions & 0 deletions packages/exa-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@corbits/exa-mcp",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"interchange": {
"tools": "./src/tools.ts"
},
"exports": {
".": {
"types": "./src/tools.ts",
"default": "./src/tools.ts"
}
},
"dependencies": {
"@corbits/mcp-adapter": "0.1.0",
"@intx/agent": "0.2.2"
}
}
52 changes: 52 additions & 0 deletions packages/exa-mcp/src/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { defineMcpToolFactory } from "@corbits/mcp-adapter";

// Static declaration of Exa's hosted MCP tool surface, including real
// per-tool argument schemas -- see the package README for what happens
// when it drifts from the server's real tool list.
//
// EXA_API_KEY is optional: unset, the server is used at its default rate
// limit; set, the key is passed through so Exa raises the caller's limit.
// The package installs, connects, and works fully with it unset.
export const exa = defineMcpToolFactory({
id: "@corbits/exa-mcp/exa",
serverName: "exa",
url: "https://mcp.exa.ai/mcp",
clientName: "corbits-code",
apiKeyEnvVar: "EXA_API_KEY",
apiKeyQueryParam: "exaApiKey",
toolDeclarations: [
{
name: "web_search",
description: "Search the web via Exa.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query." },
numResults: { type: "number", description: "Maximum results to return." },
},
required: ["query"],
},
},
{
name: "get_contents",
description: "Fetch page contents for a URL via Exa.",
inputSchema: {
type: "object",
properties: { url: { type: "string", description: "URL to fetch contents for." } },
required: ["url"],
},
},
{
name: "find_similar",
description: "Find pages similar to a URL via Exa.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to find similar pages for." },
numResults: { type: "number", description: "Maximum results to return." },
},
required: ["url"],
},
},
],
});
20 changes: 20 additions & 0 deletions packages/granola-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@corbits/granola-mcp",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"interchange": {
"tools": "./src/tools.ts"
},
"exports": {
".": {
"types": "./src/tools.ts",
"default": "./src/tools.ts"
}
},
"dependencies": {
"@corbits/mcp-adapter": "0.1.0",
"@intx/agent": "0.2.2"
}
}
39 changes: 39 additions & 0 deletions packages/granola-mcp/src/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { defineMcpToolFactory } from "@corbits/mcp-adapter";

// Static declaration of Granola's hosted MCP tool surface, including real
// per-tool argument schemas -- see the package README for what happens
// when it drifts from the server's real tool list.
export const granola = defineMcpToolFactory({
id: "@corbits/granola-mcp/granola",
serverName: "granola",
url: "https://mcp.granola.ai/mcp",
clientName: "corbits-code",
toolDeclarations: [
{
name: "list_notes",
description: "List Granola meeting notes.",
inputSchema: {
type: "object",
properties: { limit: { type: "number", description: "Maximum notes to return." } },
},
},
{
name: "get_note",
description: "Get a single Granola meeting note.",
inputSchema: {
type: "object",
properties: { id: { type: "string", description: "Note ID." } },
required: ["id"],
},
},
{
name: "search_notes",
description: "Search Granola meeting notes.",
inputSchema: {
type: "object",
properties: { query: { type: "string", description: "Free-text search query." } },
required: ["query"],
},
},
],
});
20 changes: 20 additions & 0 deletions packages/linear-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@corbits/linear-mcp",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"interchange": {
"tools": "./src/tools.ts"
},
"exports": {
".": {
"types": "./src/tools.ts",
"default": "./src/tools.ts"
}
},
"dependencies": {
"@corbits/mcp-adapter": "0.1.0",
"@intx/agent": "0.2.2"
}
}
96 changes: 96 additions & 0 deletions packages/linear-mcp/src/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { defineMcpToolFactory } from "@corbits/mcp-adapter";

// Static declaration of Linear's hosted MCP tool surface, including real
// per-tool argument schemas (not a bare open object) -- an empty schema
// gives the model no signal on what arguments a tool takes, which
// defeats the point of declaring named tools instead of a single
// dispatch-proxy tool. This list is a snapshot, not a live query -- see
// the package README for what happens when it drifts from the server's
// real tool list.
export const linear = defineMcpToolFactory({
id: "@corbits/linear-mcp/linear",
serverName: "linear",
url: "https://mcp.linear.app/mcp",
clientName: "corbits-code",
toolDeclarations: [
{
name: "list_issues",
description: "List issues in the Linear workspace.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text search over issue title/description." },
team: { type: "string", description: "Team name or ID to filter by." },
project: { type: "string", description: "Project name or ID to filter by." },
assignee: { type: "string", description: 'User ID, name, email, or "me".' },
state: { type: "string", description: "Workflow state type or name to filter by." },
limit: { type: "number", description: "Maximum results to return." },
},
},
},
{
name: "get_issue",
description: "Get a single Linear issue by id.",
inputSchema: {
type: "object",
properties: { id: { type: "string", description: "Issue ID or identifier (e.g. ENG-123)." } },
required: ["id"],
},
},
{
name: "create_issue",
description: "Create a Linear issue.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Issue title." },
team: { type: "string", description: "Team name or ID to create the issue under." },
description: { type: "string", description: "Issue description as Markdown." },
assignee: { type: "string", description: 'User ID, name, email, or "me".' },
project: { type: "string", description: "Project name or ID to add the issue to." },
},
required: ["title", "team"],
},
},
{
name: "update_issue",
description: "Update an existing Linear issue.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Issue ID or identifier to update." },
title: { type: "string", description: "New issue title." },
description: { type: "string", description: "New issue description as Markdown." },
state: { type: "string", description: "Workflow state type or name to move the issue to." },
assignee: { type: "string", description: 'User ID, name, email, or "me".' },
},
required: ["id"],
},
},
{
name: "list_projects",
description: "List projects in the Linear workspace.",
inputSchema: {
type: "object",
properties: {
team: { type: "string", description: "Team name or ID to filter by." },
query: { type: "string", description: "Free-text search over project name." },
},
},
},
{
name: "list_teams",
description: "List teams in the Linear workspace.",
inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query." } } },
},
{
name: "search_documentation",
description: "Search Linear's help documentation.",
inputSchema: {
type: "object",
properties: { query: { type: "string", description: "Free-text search query." } },
required: ["query"],
},
},
],
});
46 changes: 46 additions & 0 deletions packages/mcp-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# @corbits/mcp-adapter

Shared logic for exposing a hosted, OAuth-protected MCP server as an
`interchange.tools` tool package. Per-server packages (`@corbits/linear-mcp`,
`@corbits/slack-mcp`, `@corbits/granola-mcp`, `@corbits/exa-mcp`) are thin
configuration over `defineMcpToolFactory` -- they should not duplicate
connection, OAuth, or dispatch logic.

## Static tool declarations

`interchange.tools` factories must declare their tool names at
construction time (`ToolFactoryMeta.definitions`), before any connection to
the MCP server exists -- the deploy-time capability walk enumerates a
package's tools without instantiating it. That forces a choice, since an
MCP server's real tool list is only known after connecting:

- **Hardcoded static list per server (chosen).** Matches the constraint
above directly, keeps the model's tool menu meaningful (named tools with
real descriptions) instead of one opaque dispatch tool, and costs
nothing until the upstream server's tool list drifts.
- **Single dispatch-proxy tool.** Always correct regardless of upstream
changes, but pushes tool selection into a single tool's freeform
arguments the model has to get right blind -- worse ergonomics for no
correctness win in the common case.
- **Cache the list from a prior connection.** Accurate after first use,
but the very first install has nothing to declare (there is no prior
connection yet), and it reintroduces a locally-written file the loader
has to trust as if it were the package author's declaration.

**What happens when the real list drifts.** A tool the server has since
removed fails at `run()` with the server's own "unknown tool" error,
surfaced as a normal tool-call failure -- visible, not silent. A tool the
server has since added is invisible to the model until this package's
`toolDeclarations` is updated and republished. Keeping each server
package's declared list current with its upstream server is an ongoing
maintenance cost of this choice.

## Lazy connection

`defineMcpToolFactory` returns an `AnnotatedToolFactory` whose bundle
construction is synchronous and does no I/O. The first `run()` call lazily
connects over streamable HTTP and, if the server requires it, drives an
OAuth authorization round trip using an adapter-local callback server and
token store (`~/.corbits/mcp-auth/<server>.json`) -- the same pattern
Corbits Code's own TUI-facing MCP client uses, ported here so this package
has no import back into the parent repository's `src/` tree.
17 changes: 17 additions & 0 deletions packages/mcp-adapter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@corbits/mcp-adapter",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@intx/agent": "0.2.2",
"@modelcontextprotocol/sdk": "^1.29.0"
}
}
Loading
Loading