@strav/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +36 -0
- package/src/commands/mcp_commands.ts +114 -0
- package/src/errors.ts +11 -0
- package/src/helpers.ts +96 -0
- package/src/index.ts +35 -0
- package/src/mcp_manager.ts +264 -0
- package/src/mcp_provider.ts +43 -0
- package/src/transports/bun_http_transport.ts +42 -0
- package/src/transports/stdio.ts +30 -0
- package/src/types.ts +86 -0
- package/stubs/config/mcp.ts +24 -0
- package/tsconfig.json +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strav/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Model Context Protocol (MCP) server for the Strav framework",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./*": "./src/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"strav": {
|
|
12
|
+
"commands": "src/commands"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/",
|
|
16
|
+
"stubs/",
|
|
17
|
+
"package.json",
|
|
18
|
+
"tsconfig.json"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22
|
+
"zod": "^3.25 || ^4.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@strav/kernel": "0.1.0",
|
|
26
|
+
"@strav/http": "0.1.0",
|
|
27
|
+
"@strav/cli": "0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "bun test tests/",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"commander": "^14.0.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '@stravigor/cli'
|
|
4
|
+
import McpManager from '../mcp_manager.ts'
|
|
5
|
+
|
|
6
|
+
export function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('mcp:serve')
|
|
9
|
+
.description('Start the MCP server in stdio mode (for Claude Desktop, etc.)')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
let db
|
|
12
|
+
try {
|
|
13
|
+
const { db: database, config } = await bootstrap()
|
|
14
|
+
db = database
|
|
15
|
+
|
|
16
|
+
new McpManager(config)
|
|
17
|
+
|
|
18
|
+
// Load user registration file
|
|
19
|
+
const registerPath = McpManager.config.register
|
|
20
|
+
if (registerPath) {
|
|
21
|
+
await import(`${process.cwd()}/${registerPath}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tools = McpManager.registeredTools()
|
|
25
|
+
const resources = McpManager.registeredResources()
|
|
26
|
+
const prompts = McpManager.registeredPrompts()
|
|
27
|
+
|
|
28
|
+
// Log to stderr (stdout is the MCP protocol channel)
|
|
29
|
+
console.error(
|
|
30
|
+
chalk.dim(
|
|
31
|
+
`MCP server starting (${tools.length} tools, ${resources.length} resources, ${prompts.length} prompts)`
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const { serveStdio } = await import('../transports/stdio.ts')
|
|
36
|
+
await serveStdio()
|
|
37
|
+
|
|
38
|
+
console.error(chalk.dim('MCP server closed.'))
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
} finally {
|
|
43
|
+
McpManager.reset()
|
|
44
|
+
if (db) await shutdown(db)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('mcp:list')
|
|
50
|
+
.description('List all registered MCP tools, resources, and prompts')
|
|
51
|
+
.action(async () => {
|
|
52
|
+
let db
|
|
53
|
+
try {
|
|
54
|
+
const { db: database, config } = await bootstrap()
|
|
55
|
+
db = database
|
|
56
|
+
|
|
57
|
+
new McpManager(config)
|
|
58
|
+
|
|
59
|
+
// Load user registration file
|
|
60
|
+
const registerPath = McpManager.config.register
|
|
61
|
+
if (registerPath) {
|
|
62
|
+
await import(`${process.cwd()}/${registerPath}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tools = McpManager.registeredTools()
|
|
66
|
+
const resources = McpManager.registeredResources()
|
|
67
|
+
const prompts = McpManager.registeredPrompts()
|
|
68
|
+
|
|
69
|
+
console.log(chalk.bold('\nMCP Server Registry\n'))
|
|
70
|
+
|
|
71
|
+
// Tools
|
|
72
|
+
console.log(chalk.cyan(`Tools (${tools.length}):`))
|
|
73
|
+
if (tools.length === 0) {
|
|
74
|
+
console.log(chalk.dim(' (none)'))
|
|
75
|
+
} else {
|
|
76
|
+
for (const name of tools) {
|
|
77
|
+
const reg = McpManager.getToolRegistration(name)!
|
|
78
|
+
console.log(` ${chalk.green(name)} ${chalk.dim(reg.description ?? '')}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log()
|
|
82
|
+
|
|
83
|
+
// Resources
|
|
84
|
+
console.log(chalk.cyan(`Resources (${resources.length}):`))
|
|
85
|
+
if (resources.length === 0) {
|
|
86
|
+
console.log(chalk.dim(' (none)'))
|
|
87
|
+
} else {
|
|
88
|
+
for (const uri of resources) {
|
|
89
|
+
const reg = McpManager.getResourceRegistration(uri)!
|
|
90
|
+
console.log(` ${chalk.green(uri)} ${chalk.dim(reg.description ?? '')}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log()
|
|
94
|
+
|
|
95
|
+
// Prompts
|
|
96
|
+
console.log(chalk.cyan(`Prompts (${prompts.length}):`))
|
|
97
|
+
if (prompts.length === 0) {
|
|
98
|
+
console.log(chalk.dim(' (none)'))
|
|
99
|
+
} else {
|
|
100
|
+
for (const name of prompts) {
|
|
101
|
+
const reg = McpManager.getPromptRegistration(name)!
|
|
102
|
+
console.log(` ${chalk.green(name)} ${chalk.dim(reg.description ?? '')}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log()
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
108
|
+
process.exit(1)
|
|
109
|
+
} finally {
|
|
110
|
+
McpManager.reset()
|
|
111
|
+
if (db) await shutdown(db)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StravError } from '@stravigor/kernel'
|
|
2
|
+
|
|
3
|
+
/** Base error class for all MCP errors. */
|
|
4
|
+
export class McpError extends StravError {}
|
|
5
|
+
|
|
6
|
+
/** Thrown when a tool, resource, or prompt with the same name is registered twice. */
|
|
7
|
+
export class DuplicateRegistrationError extends McpError {
|
|
8
|
+
constructor(type: 'tool' | 'resource' | 'prompt', name: string) {
|
|
9
|
+
super(`${type} "${name}" is already registered.`)
|
|
10
|
+
}
|
|
11
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import McpManager from './mcp_manager.ts'
|
|
2
|
+
import type {
|
|
3
|
+
ZodRawShape,
|
|
4
|
+
ToolHandler,
|
|
5
|
+
ToolRegistration,
|
|
6
|
+
ResourceHandler,
|
|
7
|
+
ResourceRegistration,
|
|
8
|
+
PromptHandler,
|
|
9
|
+
PromptRegistration,
|
|
10
|
+
} from './types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MCP helper — the primary convenience API.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { mcp } from '@stravigor/mcp'
|
|
17
|
+
* import { z } from 'zod'
|
|
18
|
+
*
|
|
19
|
+
* mcp.tool('get-user', {
|
|
20
|
+
* description: 'Fetch a user by ID',
|
|
21
|
+
* input: { id: z.number() },
|
|
22
|
+
* handler: async ({ id }, { app }) => {
|
|
23
|
+
* const db = app.resolve(Database)
|
|
24
|
+
* const [user] = await db.sql`SELECT * FROM users WHERE id = ${id}`
|
|
25
|
+
* return { content: [{ type: 'text', text: JSON.stringify(user) }] }
|
|
26
|
+
* }
|
|
27
|
+
* })
|
|
28
|
+
*/
|
|
29
|
+
export const mcp = {
|
|
30
|
+
/** Register a tool that AI clients can invoke. */
|
|
31
|
+
tool<TShape extends ZodRawShape>(
|
|
32
|
+
name: string,
|
|
33
|
+
options: {
|
|
34
|
+
description?: string
|
|
35
|
+
input?: TShape
|
|
36
|
+
handler: ToolHandler<TShape>
|
|
37
|
+
}
|
|
38
|
+
): void {
|
|
39
|
+
McpManager.tool(name, options)
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Register a resource that AI clients can read. */
|
|
43
|
+
resource(
|
|
44
|
+
uri: string,
|
|
45
|
+
options: {
|
|
46
|
+
name?: string
|
|
47
|
+
description?: string
|
|
48
|
+
mimeType?: string
|
|
49
|
+
handler: ResourceHandler
|
|
50
|
+
}
|
|
51
|
+
): void {
|
|
52
|
+
McpManager.resource(uri, options)
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/** Register a prompt template that AI clients can use. */
|
|
56
|
+
prompt<TShape extends ZodRawShape>(
|
|
57
|
+
name: string,
|
|
58
|
+
options: {
|
|
59
|
+
description?: string
|
|
60
|
+
args?: TShape
|
|
61
|
+
handler: PromptHandler<TShape>
|
|
62
|
+
}
|
|
63
|
+
): void {
|
|
64
|
+
McpManager.prompt(name, options)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** List all registered tool names. */
|
|
68
|
+
registeredTools(): string[] {
|
|
69
|
+
return McpManager.registeredTools()
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** List all registered resource URIs. */
|
|
73
|
+
registeredResources(): string[] {
|
|
74
|
+
return McpManager.registeredResources()
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/** List all registered prompt names. */
|
|
78
|
+
registeredPrompts(): string[] {
|
|
79
|
+
return McpManager.registeredPrompts()
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/** Get a tool registration by name. */
|
|
83
|
+
getToolRegistration(name: string): ToolRegistration | undefined {
|
|
84
|
+
return McpManager.getToolRegistration(name)
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/** Get a resource registration by URI. */
|
|
88
|
+
getResourceRegistration(uri: string): ResourceRegistration | undefined {
|
|
89
|
+
return McpManager.getResourceRegistration(uri)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** Get a prompt registration by name. */
|
|
93
|
+
getPromptRegistration(name: string): PromptRegistration | undefined {
|
|
94
|
+
return McpManager.getPromptRegistration(name)
|
|
95
|
+
},
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Manager
|
|
2
|
+
export { default, default as McpManager } from './mcp_manager.ts'
|
|
3
|
+
|
|
4
|
+
// Provider
|
|
5
|
+
export { default as McpProvider } from './mcp_provider.ts'
|
|
6
|
+
export type { McpProviderOptions } from './mcp_provider.ts'
|
|
7
|
+
|
|
8
|
+
// Helper
|
|
9
|
+
export { mcp } from './helpers.ts'
|
|
10
|
+
|
|
11
|
+
// Transports
|
|
12
|
+
export { serveStdio } from './transports/stdio.ts'
|
|
13
|
+
export { mountHttpTransport } from './transports/bun_http_transport.ts'
|
|
14
|
+
export type { WebStandardStreamableHTTPServerTransport } from './transports/bun_http_transport.ts'
|
|
15
|
+
|
|
16
|
+
// Errors
|
|
17
|
+
export { McpError, DuplicateRegistrationError } from './errors.ts'
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export type {
|
|
21
|
+
McpConfig,
|
|
22
|
+
ZodRawShape,
|
|
23
|
+
InferShape,
|
|
24
|
+
ToolHandlerContext,
|
|
25
|
+
ToolRegistration,
|
|
26
|
+
ToolHandler,
|
|
27
|
+
ResourceRegistration,
|
|
28
|
+
ResourceHandler,
|
|
29
|
+
PromptRegistration,
|
|
30
|
+
PromptHandler,
|
|
31
|
+
// Re-exported SDK result types
|
|
32
|
+
CallToolResult,
|
|
33
|
+
GetPromptResult,
|
|
34
|
+
ReadResourceResult,
|
|
35
|
+
} from './types.ts'
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { inject, app, Configuration, Emitter, ConfigurationError } from '@stravigor/kernel'
|
|
2
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
+
import type {
|
|
4
|
+
McpConfig,
|
|
5
|
+
ZodRawShape,
|
|
6
|
+
ToolRegistration,
|
|
7
|
+
ToolHandler,
|
|
8
|
+
ToolHandlerContext,
|
|
9
|
+
ResourceRegistration,
|
|
10
|
+
ResourceHandler,
|
|
11
|
+
PromptRegistration,
|
|
12
|
+
PromptHandler,
|
|
13
|
+
} from './types.ts'
|
|
14
|
+
import { DuplicateRegistrationError } from './errors.ts'
|
|
15
|
+
|
|
16
|
+
@inject
|
|
17
|
+
export default class McpManager {
|
|
18
|
+
private static _config: McpConfig
|
|
19
|
+
private static _server: McpServer | null = null
|
|
20
|
+
private static _tools = new Map<string, ToolRegistration>()
|
|
21
|
+
private static _resources = new Map<string, ResourceRegistration>()
|
|
22
|
+
private static _prompts = new Map<string, PromptRegistration>()
|
|
23
|
+
|
|
24
|
+
constructor(config: Configuration) {
|
|
25
|
+
McpManager._config = {
|
|
26
|
+
name: (config.get('mcp.name') ?? config.get('app.name', 'Strav MCP Server')) as string,
|
|
27
|
+
version: config.get('mcp.version', '1.0.0') as string,
|
|
28
|
+
register: config.get('mcp.register') as string | undefined,
|
|
29
|
+
http: {
|
|
30
|
+
enabled: config.get('mcp.http.enabled', true) as boolean,
|
|
31
|
+
path: config.get('mcp.http.path', '/mcp') as string,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Configuration ──────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
static get config(): McpConfig {
|
|
39
|
+
if (!McpManager._config) {
|
|
40
|
+
throw new ConfigurationError(
|
|
41
|
+
'McpManager not configured. Resolve it through the container first.'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return McpManager._config
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Builder API ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Register a tool that AI clients can invoke. */
|
|
50
|
+
static tool<TShape extends ZodRawShape>(
|
|
51
|
+
name: string,
|
|
52
|
+
options: {
|
|
53
|
+
description?: string
|
|
54
|
+
input?: TShape
|
|
55
|
+
handler: ToolHandler<TShape>
|
|
56
|
+
}
|
|
57
|
+
): void {
|
|
58
|
+
if (McpManager._tools.has(name)) {
|
|
59
|
+
throw new DuplicateRegistrationError('tool', name)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
McpManager._tools.set(name, {
|
|
63
|
+
name,
|
|
64
|
+
description: options.description,
|
|
65
|
+
input: options.input,
|
|
66
|
+
handler: options.handler as ToolHandler,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
Emitter.emit('mcp:tool-registered', { name })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Register a resource that AI clients can read. */
|
|
73
|
+
static resource(
|
|
74
|
+
uri: string,
|
|
75
|
+
options: {
|
|
76
|
+
name?: string
|
|
77
|
+
description?: string
|
|
78
|
+
mimeType?: string
|
|
79
|
+
handler: ResourceHandler
|
|
80
|
+
}
|
|
81
|
+
): void {
|
|
82
|
+
if (McpManager._resources.has(uri)) {
|
|
83
|
+
throw new DuplicateRegistrationError('resource', uri)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
McpManager._resources.set(uri, {
|
|
87
|
+
uri,
|
|
88
|
+
name: options.name,
|
|
89
|
+
description: options.description,
|
|
90
|
+
mimeType: options.mimeType,
|
|
91
|
+
handler: options.handler,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
Emitter.emit('mcp:resource-registered', { uri })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Register a prompt template that AI clients can use. */
|
|
98
|
+
static prompt<TShape extends ZodRawShape>(
|
|
99
|
+
name: string,
|
|
100
|
+
options: {
|
|
101
|
+
description?: string
|
|
102
|
+
args?: TShape
|
|
103
|
+
handler: PromptHandler<TShape>
|
|
104
|
+
}
|
|
105
|
+
): void {
|
|
106
|
+
if (McpManager._prompts.has(name)) {
|
|
107
|
+
throw new DuplicateRegistrationError('prompt', name)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
McpManager._prompts.set(name, {
|
|
111
|
+
name,
|
|
112
|
+
description: options.description,
|
|
113
|
+
args: options.args,
|
|
114
|
+
handler: options.handler as PromptHandler,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
Emitter.emit('mcp:prompt-registered', { name })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Server ─────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get or create the MCP server instance.
|
|
124
|
+
*
|
|
125
|
+
* Lazily creates the server and wires all registered tools, resources,
|
|
126
|
+
* and prompts. Handlers are wrapped to inject the Application context.
|
|
127
|
+
*/
|
|
128
|
+
static getServer(): McpServer {
|
|
129
|
+
if (McpManager._server) return McpManager._server
|
|
130
|
+
|
|
131
|
+
const server = new McpServer({
|
|
132
|
+
name: McpManager.config.name,
|
|
133
|
+
version: McpManager.config.version,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const ctx: ToolHandlerContext = { app }
|
|
137
|
+
|
|
138
|
+
// Wire tools — cast at SDK boundary since our handler signature
|
|
139
|
+
// adds the DI context param that the SDK doesn't know about
|
|
140
|
+
for (const [name, reg] of McpManager._tools) {
|
|
141
|
+
const toolCb = async (params: any) => {
|
|
142
|
+
const result = await reg.handler(params ?? {}, ctx)
|
|
143
|
+
await Emitter.emit('mcp:tool-called', { name, params })
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (reg.input) {
|
|
148
|
+
server.registerTool(
|
|
149
|
+
name,
|
|
150
|
+
{
|
|
151
|
+
description: reg.description,
|
|
152
|
+
inputSchema: reg.input,
|
|
153
|
+
},
|
|
154
|
+
toolCb as any
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
server.registerTool(
|
|
158
|
+
name,
|
|
159
|
+
{
|
|
160
|
+
description: reg.description,
|
|
161
|
+
},
|
|
162
|
+
toolCb as any
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Wire resources
|
|
168
|
+
for (const [, reg] of McpManager._resources) {
|
|
169
|
+
const isTemplate = reg.uri.includes('{')
|
|
170
|
+
const metadata = {
|
|
171
|
+
title: reg.name,
|
|
172
|
+
description: reg.description,
|
|
173
|
+
mimeType: reg.mimeType,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isTemplate) {
|
|
177
|
+
server.registerResource(
|
|
178
|
+
reg.name ?? reg.uri,
|
|
179
|
+
new ResourceTemplate(reg.uri, { list: undefined }),
|
|
180
|
+
metadata,
|
|
181
|
+
(async (uri: URL, params: Record<string, string>) => {
|
|
182
|
+
const result = await reg.handler(uri, params, ctx)
|
|
183
|
+
await Emitter.emit('mcp:resource-read', { uri: uri.href })
|
|
184
|
+
return result
|
|
185
|
+
}) as any
|
|
186
|
+
)
|
|
187
|
+
} else {
|
|
188
|
+
server.registerResource(reg.name ?? reg.uri, reg.uri, metadata, (async (uri: URL) => {
|
|
189
|
+
const result = await reg.handler(uri, {}, ctx)
|
|
190
|
+
await Emitter.emit('mcp:resource-read', { uri: uri.href })
|
|
191
|
+
return result
|
|
192
|
+
}) as any)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Wire prompts
|
|
197
|
+
for (const [name, reg] of McpManager._prompts) {
|
|
198
|
+
const promptCb = async (args: any) => {
|
|
199
|
+
const result = await reg.handler(args ?? {}, ctx)
|
|
200
|
+
await Emitter.emit('mcp:prompt-called', { name, args })
|
|
201
|
+
return result
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (reg.args) {
|
|
205
|
+
server.registerPrompt(
|
|
206
|
+
name,
|
|
207
|
+
{
|
|
208
|
+
description: reg.description,
|
|
209
|
+
argsSchema: reg.args,
|
|
210
|
+
},
|
|
211
|
+
promptCb as any
|
|
212
|
+
)
|
|
213
|
+
} else {
|
|
214
|
+
server.registerPrompt(
|
|
215
|
+
name,
|
|
216
|
+
{
|
|
217
|
+
description: reg.description,
|
|
218
|
+
},
|
|
219
|
+
promptCb as any
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
McpManager._server = server
|
|
225
|
+
return server
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Inspection ─────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
static registeredTools(): string[] {
|
|
231
|
+
return Array.from(McpManager._tools.keys())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static registeredResources(): string[] {
|
|
235
|
+
return Array.from(McpManager._resources.keys())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static registeredPrompts(): string[] {
|
|
239
|
+
return Array.from(McpManager._prompts.keys())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static getToolRegistration(name: string): ToolRegistration | undefined {
|
|
243
|
+
return McpManager._tools.get(name)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static getResourceRegistration(uri: string): ResourceRegistration | undefined {
|
|
247
|
+
return McpManager._resources.get(uri)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static getPromptRegistration(name: string): PromptRegistration | undefined {
|
|
251
|
+
return McpManager._prompts.get(name)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Reset ──────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
/** Reset all state. Intended for test teardown. */
|
|
257
|
+
static reset(): void {
|
|
258
|
+
McpManager._tools.clear()
|
|
259
|
+
McpManager._resources.clear()
|
|
260
|
+
McpManager._prompts.clear()
|
|
261
|
+
McpManager._server = null
|
|
262
|
+
McpManager._config = undefined as any
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ServiceProvider } from '@stravigor/kernel'
|
|
2
|
+
import type { Application } from '@stravigor/kernel'
|
|
3
|
+
import { Router } from '@stravigor/http'
|
|
4
|
+
import McpManager from './mcp_manager.ts'
|
|
5
|
+
import { mountHttpTransport } from './transports/bun_http_transport.ts'
|
|
6
|
+
|
|
7
|
+
export interface McpProviderOptions {
|
|
8
|
+
/** Auto-mount HTTP transport on the router. Default: `true` */
|
|
9
|
+
mountHttp?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class McpProvider extends ServiceProvider {
|
|
13
|
+
readonly name = 'mcp'
|
|
14
|
+
override readonly dependencies = ['config']
|
|
15
|
+
|
|
16
|
+
constructor(private options?: McpProviderOptions) {
|
|
17
|
+
super()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override register(app: Application): void {
|
|
21
|
+
app.singleton(McpManager)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override async boot(app: Application): Promise<void> {
|
|
25
|
+
app.resolve(McpManager)
|
|
26
|
+
|
|
27
|
+
// Load user registration file if configured
|
|
28
|
+
const registerPath = McpManager.config.register
|
|
29
|
+
if (registerPath) {
|
|
30
|
+
await import(`${process.cwd()}/${registerPath}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Mount HTTP transport on the router
|
|
34
|
+
if (this.options?.mountHttp !== false && McpManager.config.http.enabled) {
|
|
35
|
+
const router = app.resolve(Router)
|
|
36
|
+
mountHttpTransport(router)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override shutdown(): void {
|
|
41
|
+
McpManager.reset()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
|
|
2
|
+
import type { Router, Context } from '@stravigor/http'
|
|
3
|
+
import { Emitter } from '@stravigor/kernel'
|
|
4
|
+
import McpManager from '../mcp_manager.ts'
|
|
5
|
+
|
|
6
|
+
export type { WebStandardStreamableHTTPServerTransport }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mount the MCP server on a Stravigor router via Streamable HTTP.
|
|
10
|
+
*
|
|
11
|
+
* Uses the SDK's `WebStandardStreamableHTTPServerTransport` which works
|
|
12
|
+
* natively with Bun's Web Standard Request/Response API.
|
|
13
|
+
*
|
|
14
|
+
* Registers handlers at the configured path (default: `/mcp`) for
|
|
15
|
+
* POST (requests), GET (SSE), and DELETE (session termination).
|
|
16
|
+
*/
|
|
17
|
+
export function mountHttpTransport(router: Router): WebStandardStreamableHTTPServerTransport {
|
|
18
|
+
const path = McpManager.config.http.path
|
|
19
|
+
|
|
20
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
21
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const server = McpManager.getServer()
|
|
25
|
+
server.connect(transport)
|
|
26
|
+
|
|
27
|
+
// The SDK transport handles POST/GET/DELETE routing internally
|
|
28
|
+
// via handleRequest(req: Request): Promise<Response>.
|
|
29
|
+
const handler = async (ctx: Context) => {
|
|
30
|
+
const response = await transport.handleRequest(ctx.request)
|
|
31
|
+
await Emitter.emit('mcp:http-request', { method: ctx.method, path })
|
|
32
|
+
return response
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
router.post(path, handler)
|
|
36
|
+
router.get(path, handler)
|
|
37
|
+
router.delete(path, handler)
|
|
38
|
+
|
|
39
|
+
Emitter.emit('mcp:http-mounted', { path })
|
|
40
|
+
|
|
41
|
+
return transport
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
2
|
+
import { Emitter } from '@stravigor/kernel'
|
|
3
|
+
import McpManager from '../mcp_manager.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Start the MCP server in stdio mode.
|
|
7
|
+
*
|
|
8
|
+
* Reads JSON-RPC messages from stdin, writes responses to stdout.
|
|
9
|
+
* Blocks until the AI client disconnects.
|
|
10
|
+
*
|
|
11
|
+
* Used by the `mcp:serve` CLI command. Claude Desktop spawns
|
|
12
|
+
* the process and communicates over stdio.
|
|
13
|
+
*/
|
|
14
|
+
export async function serveStdio(): Promise<void> {
|
|
15
|
+
const server = McpManager.getServer()
|
|
16
|
+
const transport = new StdioServerTransport()
|
|
17
|
+
|
|
18
|
+
await Emitter.emit('mcp:stdio-starting')
|
|
19
|
+
|
|
20
|
+
await server.connect(transport)
|
|
21
|
+
|
|
22
|
+
await Emitter.emit('mcp:stdio-connected')
|
|
23
|
+
|
|
24
|
+
// Block until the transport closes (AI client disconnects)
|
|
25
|
+
await new Promise<void>(resolve => {
|
|
26
|
+
transport.onclose = () => {
|
|
27
|
+
Emitter.emit('mcp:stdio-closed').then(resolve)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
import type { Application } from '@stravigor/kernel'
|
|
3
|
+
import type {
|
|
4
|
+
CallToolResult,
|
|
5
|
+
GetPromptResult,
|
|
6
|
+
ReadResourceResult,
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
8
|
+
|
|
9
|
+
// Re-export SDK result types for user convenience
|
|
10
|
+
export type { CallToolResult, GetPromptResult, ReadResourceResult }
|
|
11
|
+
|
|
12
|
+
// ── Configuration ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface McpConfig {
|
|
15
|
+
/** Server name shown to AI clients. Defaults to app name. */
|
|
16
|
+
name: string
|
|
17
|
+
/** Server version. */
|
|
18
|
+
version: string
|
|
19
|
+
/** Path to a file that registers tools/resources/prompts (for CLI). */
|
|
20
|
+
register?: string
|
|
21
|
+
/** HTTP transport settings. */
|
|
22
|
+
http: {
|
|
23
|
+
/** Whether to enable HTTP transport. */
|
|
24
|
+
enabled: boolean
|
|
25
|
+
/** Mount path for the MCP endpoint. */
|
|
26
|
+
path: string
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Handler Context ──────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface ToolHandlerContext {
|
|
33
|
+
/** The Application container — resolve any service via DI. */
|
|
34
|
+
app: Application
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Tool ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Raw Zod shape: `{ id: z.number(), name: z.string() }`. */
|
|
40
|
+
export type ZodRawShape = Record<string, z.ZodTypeAny>
|
|
41
|
+
|
|
42
|
+
/** Infer the output type from a Zod raw shape. */
|
|
43
|
+
export type InferShape<T extends ZodRawShape> = { [K in keyof T]: z.infer<T[K]> }
|
|
44
|
+
|
|
45
|
+
export interface ToolRegistration<TShape extends ZodRawShape = ZodRawShape> {
|
|
46
|
+
name: string
|
|
47
|
+
description?: string
|
|
48
|
+
input?: TShape
|
|
49
|
+
handler: ToolHandler<TShape>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ToolHandler<TShape extends ZodRawShape = ZodRawShape> = (
|
|
53
|
+
params: InferShape<TShape>,
|
|
54
|
+
ctx: ToolHandlerContext
|
|
55
|
+
) => CallToolResult | Promise<CallToolResult>
|
|
56
|
+
|
|
57
|
+
// ── Resource ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export interface ResourceRegistration {
|
|
60
|
+
/** URI or URI template, e.g., `'file:///config'` or `'strav://models/{name}'`. */
|
|
61
|
+
uri: string
|
|
62
|
+
name?: string
|
|
63
|
+
description?: string
|
|
64
|
+
mimeType?: string
|
|
65
|
+
handler: ResourceHandler
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ResourceHandler = (
|
|
69
|
+
uri: URL,
|
|
70
|
+
params: Record<string, string>,
|
|
71
|
+
ctx: ToolHandlerContext
|
|
72
|
+
) => ReadResourceResult | Promise<ReadResourceResult>
|
|
73
|
+
|
|
74
|
+
// ── Prompt ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface PromptRegistration<TShape extends ZodRawShape = ZodRawShape> {
|
|
77
|
+
name: string
|
|
78
|
+
description?: string
|
|
79
|
+
args?: TShape
|
|
80
|
+
handler: PromptHandler<TShape>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type PromptHandler<TShape extends ZodRawShape = ZodRawShape> = (
|
|
84
|
+
args: InferShape<TShape>,
|
|
85
|
+
ctx: ToolHandlerContext
|
|
86
|
+
) => GetPromptResult | Promise<GetPromptResult>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { env } from '@stravigor/kernel/helpers'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
/** Server name shown to AI clients. Defaults to app name if not set. */
|
|
5
|
+
name: env('MCP_NAME', undefined),
|
|
6
|
+
|
|
7
|
+
/** Server version. */
|
|
8
|
+
version: env('MCP_VERSION', '1.0.0'),
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Path to the file that registers tools, resources, and prompts.
|
|
12
|
+
* This file is imported automatically by the provider and CLI commands.
|
|
13
|
+
*/
|
|
14
|
+
register: 'mcp/server.ts',
|
|
15
|
+
|
|
16
|
+
/** HTTP transport settings (for hosted deployments). */
|
|
17
|
+
http: {
|
|
18
|
+
/** Enable the HTTP transport. Mounts an MCP endpoint on the router. */
|
|
19
|
+
enabled: env('MCP_HTTP', 'true').bool(),
|
|
20
|
+
|
|
21
|
+
/** Mount path for the MCP endpoint. */
|
|
22
|
+
path: env('MCP_PATH', '/mcp'),
|
|
23
|
+
},
|
|
24
|
+
}
|