Skip to content

Commit 2f2dada

Browse files
committed
MCP documentation
1 parent 8515822 commit 2f2dada

8 files changed

Lines changed: 459 additions & 216 deletions

File tree

service/src/mcp/tools/env-variable-tools.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ export const registerEnvVariableTools = (mcp: McpServer, _injector: Injector, el
1515
mcp.registerTool(
1616
'set_stack_env_variable',
1717
{
18-
description: 'Set or update a single environment variable on a stack',
18+
description:
19+
'Set or update a single stack-level environment variable. Stack env variables are inherited by all services unless overridden per-service.',
1920
inputSchema: {
20-
stackName: z.string(),
21-
variableName: z.string(),
22-
value: environmentVariableValueSchema,
21+
stackName: z.string().describe('Name of the stack'),
22+
variableName: z.string().describe('Environment variable name (e.g. "DATABASE_URL")'),
23+
value: environmentVariableValueSchema.describe('How the variable value is resolved'),
2324
},
25+
annotations: { idempotentHint: true },
2426
},
2527
async ({ stackName, variableName, value }) => {
2628
try {
@@ -48,11 +50,13 @@ export const registerEnvVariableTools = (mcp: McpServer, _injector: Injector, el
4850
mcp.registerTool(
4951
'remove_stack_env_variable',
5052
{
51-
description: 'Remove a single environment variable from a stack',
53+
description:
54+
'Remove a single environment variable from a stack. Services that relied on this variable will no longer receive it unless they have their own override.',
5255
inputSchema: {
53-
stackName: z.string(),
54-
variableName: z.string(),
56+
stackName: z.string().describe('Name of the stack'),
57+
variableName: z.string().describe('Environment variable name to remove'),
5558
},
59+
annotations: { destructiveHint: true },
5660
},
5761
async ({ stackName, variableName }) => {
5862
try {
@@ -76,12 +80,14 @@ export const registerEnvVariableTools = (mcp: McpServer, _injector: Injector, el
7680
mcp.registerTool(
7781
'set_service_env_override',
7882
{
79-
description: 'Set or update a single environment variable override on a service',
83+
description:
84+
'Set or update a per-service environment variable override. Overrides the stack-level default for this variable on this service only.',
8085
inputSchema: {
81-
serviceId: z.string(),
82-
variableName: z.string(),
83-
value: environmentVariableValueSchema,
86+
serviceId: z.string().describe('UUID of the service'),
87+
variableName: z.string().describe('Environment variable name (e.g. "PORT")'),
88+
value: environmentVariableValueSchema.describe('How the variable value is resolved'),
8489
},
90+
annotations: { idempotentHint: true },
8591
},
8692
async ({ serviceId, variableName, value }) => {
8793
try {
@@ -109,11 +115,13 @@ export const registerEnvVariableTools = (mcp: McpServer, _injector: Injector, el
109115
mcp.registerTool(
110116
'remove_service_env_override',
111117
{
112-
description: 'Remove a single environment variable override from a service',
118+
description:
119+
'Remove a per-service environment variable override. The service will fall back to the stack-level default for this variable.',
113120
inputSchema: {
114-
serviceId: z.string(),
115-
variableName: z.string(),
121+
serviceId: z.string().describe('UUID of the service'),
122+
variableName: z.string().describe('Environment variable name to remove the override for'),
116123
},
124+
annotations: { destructiveHint: true },
117125
},
118126
async ({ serviceId, variableName }) => {
119127
try {

service/src/mcp/tools/mcp-helpers.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
23
import type { Injector } from '@furystack/inject'
34
import { z } from 'zod'
45

@@ -12,9 +13,14 @@ export const errorResult = (text: string): TextResult => ({ content: [{ type: 't
1213
export const mcpTrigger = { triggeredBy: 'mcp-user', triggerSource: 'mcp' as const }
1314

1415
export const environmentVariableValueSchema = z.object({
15-
source: z.enum(['inherit', 'custom']),
16-
customValue: z.string().optional(),
17-
isSensitive: z.boolean().optional(),
16+
source: z
17+
.enum(['inherit', 'custom'])
18+
.describe("'inherit' uses the value from the host system environment; 'custom' uses the provided customValue"),
19+
customValue: z
20+
.string()
21+
.optional()
22+
.describe("The value to use when source is 'custom'. Ignored when source is 'inherit'."),
23+
isSensitive: z.boolean().optional().describe('When true, the value is encrypted at rest and masked in API responses'),
1824
})
1925

2026
export const registerServiceAction = (
@@ -33,13 +39,22 @@ export const registerServiceAction = (
3339
| 'updateService'
3440
>,
3541
pastTense: string,
42+
annotations?: ToolAnnotations,
3643
) => {
37-
mcp.registerTool(name, { description, inputSchema: { serviceId: z.string() } }, async ({ serviceId }) => {
38-
try {
39-
await injector.getInstance(ProcessManager)[method](serviceId, mcpTrigger)
40-
return textResult(`Service ${serviceId} ${pastTense}`)
41-
} catch (error) {
42-
return errorResult(`Failed: ${(error as Error).message}`)
43-
}
44-
})
44+
mcp.registerTool(
45+
name,
46+
{
47+
description,
48+
inputSchema: { serviceId: z.string().describe('UUID of the target service') },
49+
annotations,
50+
},
51+
async ({ serviceId }) => {
52+
try {
53+
await injector.getInstance(ProcessManager)[method](serviceId, mcpTrigger)
54+
return textResult(`Service ${serviceId} ${pastTense}`)
55+
} catch (error) {
56+
return errorResult(`Failed: ${(error as Error).message}`)
57+
}
58+
},
59+
)
4560
}

service/src/mcp/tools/prerequisite-tools.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
1313
mcp.registerTool(
1414
'list_prerequisites',
1515
{
16-
description: 'List prerequisites, optionally filtered by stack name',
16+
description:
17+
'List prerequisites (external requirements like Node.js, git, env variables). Optionally filtered by stack name.',
1718
inputSchema: {
18-
stackName: z.string().optional().describe('Filter by stack name'),
19+
stackName: z.string().optional().describe('Filter by stack name. Returns all prerequisites if omitted.'),
1920
},
21+
annotations: { readOnlyHint: true },
2022
},
2123
async ({ stackName }) => {
2224
const filter = stackName ? { stackName: { $eq: stackName } } : undefined
@@ -27,7 +29,12 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
2729

2830
mcp.registerTool(
2931
'check_prerequisite',
30-
{ description: 'Check if a prerequisite is satisfied', inputSchema: { prerequisiteId: z.string() } },
32+
{
33+
description:
34+
'Run a live check to determine if a prerequisite is satisfied on the host system (e.g. check Node.js version, verify env variable exists). Returns the check result and installation help if not satisfied.',
35+
inputSchema: { prerequisiteId: z.string().describe('UUID of the prerequisite to check') },
36+
annotations: { readOnlyHint: true, openWorldHint: true },
37+
},
3138
async ({ prerequisiteId }) => {
3239
const prereqs = await repository
3340
.getDataSetFor(Prerequisite, 'id')
@@ -51,11 +58,12 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
5158
mcp.registerTool(
5259
'create_prerequisite',
5360
{
54-
description: 'Create a new prerequisite for a stack',
61+
description:
62+
'Create a new prerequisite for a stack. Prerequisites describe external requirements that must be satisfied before services can run.',
5563
inputSchema: {
56-
id: z.string().optional().describe('UUID. Auto-generated if omitted.'),
57-
stackName: z.string(),
58-
name: z.string().describe('Human-readable name, e.g. "Node.js >= 18"'),
64+
id: z.string().optional().describe('UUID primary key. Auto-generated if omitted.'),
65+
stackName: z.string().describe('Name of the stack this prerequisite belongs to'),
66+
name: z.string().describe('Human-readable name shown in the UI (e.g. "Node.js >= 18")'),
5967
type: z
6068
.enum([
6169
'node',
@@ -68,9 +76,16 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
6876
'env-variable',
6977
'custom-script',
7078
])
71-
.describe('Prerequisite type'),
72-
config: z.record(z.string(), z.unknown()).describe('Type-specific config (e.g. { minimumVersion: "18.0.0" })'),
73-
installationHelp: z.string().optional().describe('Help text shown when check fails'),
79+
.describe('Determines the check logic and the expected shape of config'),
80+
config: z
81+
.record(z.string(), z.unknown())
82+
.describe(
83+
'Type-specific configuration. Examples: { minimumVersion: "18.0.0" } for node/yarn, { version: "8.0" } for dotnet-sdk/dotnet-runtime, { feedUrl: "...", feedName?: "..." } for nuget-feed, { variableName: "...", isSensitive?: true } for env-variable, { script: "..." } for custom-script. Empty {} for git/github-cli.',
84+
),
85+
installationHelp: z
86+
.string()
87+
.optional()
88+
.describe('Help text shown to the user when the prerequisite check fails (e.g. installation instructions)'),
7489
},
7590
},
7691
async ({ id: providedId, stackName, name, type, config, installationHelp }) => {
@@ -98,10 +113,11 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
98113
mcp.registerTool(
99114
'update_prerequisite',
100115
{
101-
description: 'Update a prerequisite (PATCH semantics)',
116+
description:
117+
'Update a prerequisite. Uses PATCH semantics: only provided fields are updated, others are left unchanged.',
102118
inputSchema: {
103-
prerequisiteId: z.string(),
104-
name: z.string().optional(),
119+
prerequisiteId: z.string().describe('UUID of the prerequisite to update'),
120+
name: z.string().optional().describe('Human-readable name shown in the UI'),
105121
type: z
106122
.enum([
107123
'node',
@@ -114,10 +130,18 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
114130
'env-variable',
115131
'custom-script',
116132
])
117-
.optional(),
118-
config: z.record(z.string(), z.unknown()).optional(),
119-
installationHelp: z.string().optional(),
133+
.optional()
134+
.describe('Determines the check logic and the expected shape of config'),
135+
config: z
136+
.record(z.string(), z.unknown())
137+
.optional()
138+
.describe('Type-specific configuration. Must match the expected shape for the prerequisite type.'),
139+
installationHelp: z
140+
.string()
141+
.optional()
142+
.describe('Help text shown to the user when the prerequisite check fails'),
120143
},
144+
annotations: { idempotentHint: true },
121145
},
122146
async ({ prerequisiteId, name, type, config, installationHelp }) => {
123147
try {
@@ -140,8 +164,9 @@ export const registerPrerequisiteTools = (mcp: McpServer, _injector: Injector, e
140164
mcp.registerTool(
141165
'delete_prerequisite',
142166
{
143-
description: 'Delete a prerequisite',
144-
inputSchema: { prerequisiteId: z.string() },
167+
description: 'Delete a prerequisite. Also removes any service-prerequisite links that reference it.',
168+
inputSchema: { prerequisiteId: z.string().describe('UUID of the prerequisite to delete') },
169+
annotations: { destructiveHint: true },
145170
},
146171
async ({ prerequisiteId }) => {
147172
try {

service/src/mcp/tools/repository-tools.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
1717
mcp.registerTool(
1818
'list_repositories',
1919
{
20-
description: 'List GitHub repositories, optionally filtered by stack name',
20+
description:
21+
'List GitHub repository entries. Each repository links a git URL to a stack and can be referenced by services via repositoryId.',
2122
inputSchema: {
22-
stackName: z.string().optional().describe('Filter by stack name'),
23+
stackName: z.string().optional().describe('Filter by stack name. Returns all repositories if omitted.'),
2324
},
25+
annotations: { readOnlyHint: true },
2426
},
2527
async ({ stackName }) => {
2628
const filter = stackName ? { stackName: { $eq: stackName } } : undefined
@@ -32,8 +34,9 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
3234
mcp.registerTool(
3335
'get_repository',
3436
{
35-
description: 'Get a single GitHub repository by ID',
36-
inputSchema: { repositoryId: z.string() },
37+
description: 'Get a single GitHub repository entry by ID.',
38+
inputSchema: { repositoryId: z.string().describe('UUID of the repository to retrieve') },
39+
annotations: { readOnlyHint: true },
3740
},
3841
async ({ repositoryId }) => {
3942
const results = await repository
@@ -48,13 +51,14 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
4851
mcp.registerTool(
4952
'create_repository',
5053
{
51-
description: 'Create a new GitHub repository entry for a stack',
54+
description:
55+
'Create a new GitHub repository entry for a stack. Services reference repositories via repositoryId to know which repo to clone.',
5256
inputSchema: {
53-
id: z.string().optional().describe('UUID. Auto-generated if omitted.'),
54-
stackName: z.string(),
55-
url: z.string().describe('Full URL to the git repository'),
56-
displayName: z.string(),
57-
description: z.string().optional(),
57+
id: z.string().optional().describe('UUID primary key. Auto-generated if omitted.'),
58+
stackName: z.string().describe('Name of the stack this repository belongs to'),
59+
url: z.string().describe('Full git URL (e.g. "https://github.com/user/repo")'),
60+
displayName: z.string().describe('Human-readable name shown in the UI'),
61+
description: z.string().optional().describe('What this repository contains'),
5862
},
5963
},
6064
async ({ id: providedId, stackName, url, displayName, description }) => {
@@ -81,13 +85,14 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
8185
mcp.registerTool(
8286
'update_repository',
8387
{
84-
description: 'Update a GitHub repository entry (PATCH semantics)',
88+
description: 'Update a GitHub repository entry. Uses PATCH semantics: only provided fields are updated.',
8589
inputSchema: {
86-
repositoryId: z.string(),
87-
url: z.string().optional(),
88-
displayName: z.string().optional(),
89-
description: z.string().optional(),
90+
repositoryId: z.string().describe('UUID of the repository to update'),
91+
url: z.string().optional().describe('Full git URL (e.g. "https://github.com/user/repo")'),
92+
displayName: z.string().optional().describe('Human-readable name shown in the UI'),
93+
description: z.string().optional().describe('What this repository contains'),
9094
},
95+
annotations: { idempotentHint: true },
9196
},
9297
async ({ repositoryId, url, displayName, description }) => {
9398
try {
@@ -109,8 +114,10 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
109114
mcp.registerTool(
110115
'delete_repository',
111116
{
112-
description: 'Delete a GitHub repository entry',
113-
inputSchema: { repositoryId: z.string() },
117+
description:
118+
'Delete a GitHub repository entry. Does not remove cloned files from disk or affect services that reference it.',
119+
inputSchema: { repositoryId: z.string().describe('UUID of the repository to delete') },
120+
annotations: { destructiveHint: true },
114121
},
115122
async ({ repositoryId }) => {
116123
try {
@@ -125,8 +132,10 @@ export const registerRepositoryTools = (mcp: McpServer, _injector: Injector, ele
125132
mcp.registerTool(
126133
'validate_repository',
127134
{
128-
description: 'Check if a GitHub repository is accessible via git ls-remote',
129-
inputSchema: { repositoryId: z.string() },
135+
description:
136+
'Check if a GitHub repository is accessible by running git ls-remote against its URL. Useful for verifying credentials and network access before cloning.',
137+
inputSchema: { repositoryId: z.string().describe('UUID of the repository to validate') },
138+
annotations: { readOnlyHint: true, openWorldHint: true },
130139
},
131140
async ({ repositoryId }) => {
132141
const results = await repository

0 commit comments

Comments
 (0)