Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/calm-dodos-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'incur': patch
---

Added configurable MCP server names and titles.

```ts
Cli.create('tapimo', { mcp: { name: 'tempo', title: 'Tempo MCP' } })
```
35 changes: 33 additions & 2 deletions src/Cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,29 @@ describe('built-in commands', () => {
}
})

test('mcp add registers an overridden MCP name with the CLI command', async () => {
const spy = vi
.spyOn(SyncMcp, 'register')
.mockResolvedValue({ command: 'pnpm test --mcp', agents: ['Cursor'] })
try {
const cli = Cli.create('test', { mcp: { name: 'example' } })
cli.command('ping', { run: () => ({ pong: true }) })
const { output, exitCode } = await serve(cli, ['mcp', 'add'])

expect(exitCode).toBeUndefined()
expect(spy).toHaveBeenCalledWith('example', {
agents: [],
cli: 'test',
command: undefined,
global: true,
})
expect(output).toContain('Registered example as MCP server')
expect(output).toContain('Agents can now use example tools.')
} finally {
spy.mockRestore()
}
})

test('mcp add exits nonzero when registration fails', async () => {
const spy = vi.spyOn(SyncMcp, 'register').mockRejectedValue('register failed')
try {
Expand Down Expand Up @@ -5691,7 +5714,12 @@ describe('fetch', () => {
function mcpCli() {
const cli = Cli.create('test', {
version: '1.0.0',
mcp: { tools: { discovery: 'direct' } },
mcp: {
instructions: 'Use the example MCP.',
name: 'example',
title: 'Example MCP',
tools: { discovery: 'direct' },
},
})
cli.command('greet', {
description: 'Greet someone',
Expand Down Expand Up @@ -5766,13 +5794,16 @@ describe('fetch', () => {
expect(res.headers.get('mcp-session-id')).toBeNull()
const body = await res.json()
expect({
instructions: body.result.instructions,
serverInfo: body.result.serverInfo,
hasTools: 'tools' in (body.result.capabilities ?? {}),
}).toMatchInlineSnapshot(`
{
"hasTools": true,
"instructions": "Use the example MCP.",
"serverInfo": {
"name": "test",
"name": "example",
"title": "Example MCP",
"version": "1.0.0",
},
}
Expand Down
35 changes: 27 additions & 8 deletions src/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,10 @@ export function create(
const commands = new Map<string, CommandEntry>()
const middlewares: MiddlewareHandler[] = []
const pending: Promise<void>[] = []
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
const mcpHandler = createMcpHttpHandler(def.mcp?.name ?? name, def.version ?? '0.0.0', {
instructions: def.mcp?.instructions,
stateless: def.mcp?.stateless,
title: def.mcp?.title,
tools: def.mcp?.tools,
})

Expand Down Expand Up @@ -594,8 +596,12 @@ export declare namespace create {
command?: string | undefined
/** Instructions describing how to use the server and its features. */
instructions?: string | undefined
/** MCP server and registration name. Defaults to the CLI name. */
name?: string | undefined
/** Disable HTTP MCP session management. Defaults to `true`. */
stateless?: boolean | undefined
/** Human-readable MCP server title. */
title?: string | undefined
/** Controls how command tools are exposed to MCP clients. */
tools?: Mcp.ToolFilter | undefined
}
Expand Down Expand Up @@ -723,12 +729,13 @@ async function serveImpl(

// --mcp: start as MCP stdio server
if (mcpFlag) {
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
await Mcp.serve(options.mcp?.name ?? name, options.version ?? '0.0.0', commands, {
middlewares: options.middlewares,
env: options.envSchema,
vars: options.vars,
version: options.version,
...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
...(options.mcp?.title ? { title: options.mcp.title } : undefined),
...(options.mcp?.tools ? { tools: options.mcp.tools } : undefined),
})
return
Expand Down Expand Up @@ -1096,18 +1103,20 @@ async function serveImpl(
}

try {
const mcpName = options.mcp?.name ?? name
stdout('Registering MCP server...')
const result = await SyncMcp.register(name, {
const result = await SyncMcp.register(mcpName, {
...(mcpName === name ? undefined : { cli: name }),
command,
global,
agents,
})
stdout('\r\x1b[K')
const lines: string[] = []
lines.push(`✓ Registered ${name} as MCP server`)
lines.push(`✓ Registered ${mcpName} as MCP server`)
if (result.agents.length > 0) lines.push(` Agents: ${result.agents.join(', ')}`)
lines.push('')
lines.push(`Agents can now use ${name} tools.`)
lines.push(`Agents can now use ${mcpName} tools.`)
const suggestions = options.sync?.suggestions
if (suggestions && suggestions.length > 0) {
lines.push('')
Expand All @@ -1118,7 +1127,7 @@ async function serveImpl(
if (fullOutput || formatExplicit)
writeln(
Formatter.format(
{ name, command: result.command, agents: result.agents },
{ name: mcpName, command: result.command, agents: result.agents },
formatExplicit ? formatFlag : 'toon',
),
)
Expand Down Expand Up @@ -1840,7 +1849,10 @@ function createMcpHttpHandler(
const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } =
await import('@modelcontextprotocol/server')

const server = new McpServer({ name, version })
const server = new McpServer(
{ name, ...(options.title ? { title: options.title } : undefined), version },
options.instructions ? { instructions: options.instructions } : undefined,
)
Mcp.registerTools(server, commands, {
env: mcpOptions?.env,
fromJsonSchema,
Expand Down Expand Up @@ -1920,8 +1932,12 @@ function createMcpHttpHandler(

declare namespace createMcpHttpHandler {
type Options = {
/** Instructions describing how to use the server and its features. */
instructions?: string | undefined
/** Disable HTTP MCP session management. Defaults to `true`. */
stateless?: boolean | undefined
/** Human-readable MCP server title. */
title?: string | undefined
/** Filters which command tools are exposed to MCP clients. */
tools?: Mcp.ToolFilter | undefined
}
Expand Down Expand Up @@ -2480,7 +2496,9 @@ declare namespace serveImpl {
agents?: string[] | undefined
command?: string | undefined
instructions?: string | undefined
name?: string | undefined
stateless?: boolean | undefined
title?: string | undefined
tools?: Mcp.ToolFilter | undefined
}
| undefined
Expand Down Expand Up @@ -2817,14 +2835,15 @@ async function runMcpDoctor(
output.on('data', (chunk) => chunks.push(chunk.toString()))

let serveError: unknown
const done = Mcp.serve(name, options.version ?? '0.0.0', commands, {
const done = Mcp.serve(options.mcp?.name ?? name, options.version ?? '0.0.0', commands, {
input,
output,
middlewares: options.middlewares,
env: options.envSchema,
vars: options.vars,
version: options.version,
...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
...(options.mcp?.title ? { title: options.mcp.title } : undefined),
tools: { ...options.mcp?.tools, discovery: 'direct' },
}).catch((error) => {
serveError = error
Expand Down
27 changes: 27 additions & 0 deletions src/Mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ describe('Mcp', () => {
expect(res.result.capabilities.tools).toBeDefined()
})

test('initialize includes the server title', async () => {
const input = new PassThrough()
const output = new PassThrough()
const chunks: string[] = []
output.on('data', (chunk) => chunks.push(chunk.toString()))

const done = Mcp.serve('test-cli', '1.0.0', createTestCommands(), {
input,
output,
title: 'Test MCP',
})

input.write(
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
)
await new Promise((r) => setTimeout(r, 20))
input.end()
await done

const [res] = chunks.map((chunk) => JSON.parse(chunk.trim()))
expect(res.result.serverInfo).toEqual({
name: 'test-cli',
title: 'Test MCP',
version: '1.0.0',
})
})

test('initialize with 2025-03-26 protocol version', async () => {
const [res] = await mcpSession(createTestCommands(), [
{
Expand Down
4 changes: 3 additions & 1 deletion src/Mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function serve(
const StdioServerTransport = await importStdioServerTransport(mcp, stdio)

const server = new McpServer(
{ name, version },
{ name, ...(options.title ? { title: options.title } : undefined), version },
options.instructions ? { instructions: options.instructions } : undefined,
)

Expand Down Expand Up @@ -94,6 +94,8 @@ export declare namespace serve {
version?: string | undefined
/** Instructions describing how to use the server and its features. */
instructions?: string | undefined
/** Human-readable MCP server title. */
title?: string | undefined
/** Filters which command tools are exposed to MCP clients. */
tools?: ToolFilter | undefined
}
Expand Down
16 changes: 16 additions & 0 deletions src/SyncMcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ test('register uses runner for source entrypoints outside node_modules', async (
expect(result.command).toContain('my-cli --mcp')
})

test('register derives the command from a different CLI name', async () => {
process.argv[1] = join(tmp, 'dist', 'bin.js')

const result = await register('example', { agents: ['amp'], cli: 'my-cli' })

expect(result.command).toMatch(/^(npx|pnpx|bunx)\s/)
expect(result.command).toContain('my-cli --mcp')

const configPath = join(fakeHome!, '.config', 'amp', 'settings.json')
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
expect(config['amp.mcpServers']['example']).toEqual({
command: result.command.split(' ')[0],
args: ['my-cli', '--mcp'],
})
})

test('register uses bare name for global package entrypoints under node_modules', async () => {
process.argv[1] = join(tmp, 'global', 'node_modules', 'my-cli', 'dist', 'bin.js')

Expand Down
4 changes: 3 additions & 1 deletion src/SyncMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function register(
options: register.Options = {},
): Promise<register.Result> {
const runner = detectRunner()
const command = options.command ?? defaultCommand(name, runner)
const command = options.command ?? defaultCommand(options.cli ?? name, runner)
const targetAgents = options.agents ?? []
const ampOnly = targetAgents.length === 1 && targetAgents[0] === 'amp'

Expand Down Expand Up @@ -83,6 +83,8 @@ export declare namespace register {
type Options = {
/** Target specific agents (e.g. `'claude-code'`, `'cursor'`). */
agents?: string[] | undefined
/** CLI name used to derive the default command. Defaults to the MCP server name. */
cli?: string | undefined
/** Override the command agents will run. Defaults to `<runner> <name> --mcp`. */
command?: string | undefined
/** Install globally. Defaults to `true`. */
Expand Down
Loading