Skip to content

Commit 1295569

Browse files
committed
feat(all): add team limits and GitHub deployment features with email notifications
Add comprehensive team limits system for MCP server deployments with validation, GitHub deployment option with metrics and error handling, and deployment email notifications for success/failure events. Improve OAuth discovery with URL support, add team context composable, and enhance UI with dynamic language/runtime options and consistent empty states.
1 parent f374f35 commit 1295569

File tree

36 files changed

+1533
-466
lines changed

36 files changed

+1533
-466
lines changed

services/backend/api-spec.json

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2711,6 +2711,18 @@
27112711
"is_owner": {
27122712
"type": "boolean",
27132713
"description": "Whether the current user owns this team"
2714+
},
2715+
"allow_remote_mcp": {
2716+
"type": "boolean",
2717+
"description": "Whether remote MCP servers are allowed for this team"
2718+
},
2719+
"allow_github_mcp": {
2720+
"type": "boolean",
2721+
"description": "Whether GitHub MCP deployments are allowed for this team"
2722+
},
2723+
"allow_private_github_repos": {
2724+
"type": "boolean",
2725+
"description": "Whether private GitHub repositories are allowed for MCP deployments"
27142726
}
27152727
},
27162728
"required": [
@@ -2720,7 +2732,10 @@
27202732
"owner_id",
27212733
"is_default",
27222734
"role",
2723-
"is_owner"
2735+
"is_owner",
2736+
"allow_remote_mcp",
2737+
"allow_github_mcp",
2738+
"allow_private_github_repos"
27242739
],
27252740
"additionalProperties": false
27262741
},
@@ -10133,6 +10148,10 @@
1013310148
"type": "number",
1013410149
"description": "Number of HTTP MCP servers (http/sse transport)"
1013510150
},
10151+
"github_mcp_servers": {
10152+
"type": "number",
10153+
"description": "Current number of GitHub-deployed MCP servers"
10154+
},
1013610155
"limits": {
1013710156
"type": "object",
1013810157
"properties": {
@@ -10171,6 +10190,7 @@
1017110190
"total_installed_mcp_servers",
1017210191
"non_http_mcp_servers",
1017310192
"http_mcp_servers",
10193+
"github_mcp_servers",
1017410194
"limits"
1017510195
]
1017610196
}
@@ -15605,6 +15625,34 @@
1560515625
}
1560615626
}
1560715627
},
15628+
"404": {
15629+
"description": "Team not found",
15630+
"content": {
15631+
"application/json": {
15632+
"schema": {
15633+
"type": "object",
15634+
"properties": {
15635+
"success": {
15636+
"type": "boolean",
15637+
"default": false
15638+
},
15639+
"error": {
15640+
"type": "string"
15641+
},
15642+
"step": {
15643+
"type": "string",
15644+
"description": "The validation step that failed (e.g., validate_package_json)"
15645+
}
15646+
},
15647+
"required": [
15648+
"success",
15649+
"error"
15650+
],
15651+
"description": "Team not found"
15652+
}
15653+
}
15654+
}
15655+
},
1560815656
"500": {
1560915657
"description": "Internal Server Error",
1561015658
"content": {
@@ -41820,6 +41868,14 @@
4182041868
"type": "string",
4182141869
"description": "User ID used in processId for this specific instance"
4182241870
},
41871+
"language": {
41872+
"type": "string",
41873+
"description": "Programming language (e.g., \"typescript\", \"python\", \"go\")"
41874+
},
41875+
"runtime": {
41876+
"type": "string",
41877+
"description": "Runtime environment (e.g., \"node\", \"python\", \"docker\")"
41878+
},
4182341879
"secret_metadata": {
4182441880
"type": "object",
4182541881
"properties": {

services/backend/api-spec.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,15 @@ paths:
19021902
is_owner:
19031903
type: boolean
19041904
description: Whether the current user owns this team
1905+
allow_remote_mcp:
1906+
type: boolean
1907+
description: Whether remote MCP servers are allowed for this team
1908+
allow_github_mcp:
1909+
type: boolean
1910+
description: Whether GitHub MCP deployments are allowed for this team
1911+
allow_private_github_repos:
1912+
type: boolean
1913+
description: Whether private GitHub repositories are allowed for MCP deployments
19051914
required:
19061915
- id
19071916
- name
@@ -1910,6 +1919,9 @@ paths:
19101919
- is_default
19111920
- role
19121921
- is_owner
1922+
- allow_remote_mcp
1923+
- allow_github_mcp
1924+
- allow_private_github_repos
19131925
additionalProperties: false
19141926
description: Array of teams the user belongs to
19151927
required:
@@ -7025,6 +7037,9 @@ paths:
70257037
http_mcp_servers:
70267038
type: number
70277039
description: Number of HTTP MCP servers (http/sse transport)
7040+
github_mcp_servers:
7041+
type: number
7042+
description: Current number of GitHub-deployed MCP servers
70287043
limits:
70297044
type: object
70307045
properties:
@@ -7054,6 +7069,7 @@ paths:
70547069
- total_installed_mcp_servers
70557070
- non_http_mcp_servers
70567071
- http_mcp_servers
7072+
- github_mcp_servers
70577073
- limits
70587074
required:
70597075
- success
@@ -10882,6 +10898,25 @@ paths:
1088210898
- success
1088310899
- error
1088410900
description: Forbidden
10901+
"404":
10902+
description: Team not found
10903+
content:
10904+
application/json:
10905+
schema:
10906+
type: object
10907+
properties:
10908+
success:
10909+
type: boolean
10910+
default: false
10911+
error:
10912+
type: string
10913+
step:
10914+
type: string
10915+
description: The validation step that failed (e.g., validate_package_json)
10916+
required:
10917+
- success
10918+
- error
10919+
description: Team not found
1088510920
"500":
1088610921
description: Internal Server Error
1088710922
content:
@@ -29637,6 +29672,12 @@ paths:
2963729672
user_slug:
2963829673
type: string
2963929674
description: User ID used in processId for this specific instance
29675+
language:
29676+
type: string
29677+
description: Programming language (e.g., "typescript", "python", "go")
29678+
runtime:
29679+
type: string
29680+
description: Runtime environment (e.g., "node", "python", "docker")
2964029681
secret_metadata:
2964129682
type: object
2964229683
properties:

services/backend/src/email/emailService.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,20 @@ export class EmailService {
3030
throw new Error('SMTP configuration is not available or invalid');
3131
}
3232

33-
// Render the email template
33+
// Structured trace logging
34+
logger.trace({
35+
operation: 'rendering_template',
36+
template: validatedOptions.template,
37+
recipient: validatedOptions.to,
38+
hasVariables: !!validatedOptions.variables,
39+
variableCount: validatedOptions.variables ? Object.keys(validatedOptions.variables).length : 0
40+
}, `Rendering email template: ${validatedOptions.template}`);
41+
42+
// Render the email template (pass logger for debug visibility)
3443
const html = await TemplateRenderer.render({
3544
template: validatedOptions.template,
3645
variables: validatedOptions.variables || {},
46+
logger
3747
});
3848

3949
// Prepare email options

services/backend/src/email/templateRenderer.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,47 @@ import type { TemplateRenderOptions, TemplateValidationResult } from './types';
66

77
export class TemplateRenderer {
88
private static templateCache = new Map<string, pug.compileTemplate>();
9-
private static templatesDir = path.join(__dirname, 'templates');
10-
private static layoutsDir = path.join(__dirname, 'templates', 'layouts');
9+
10+
/**
11+
* Get templates directory path (resolved at runtime to avoid stale cached paths)
12+
*/
13+
private static getTemplatesDir(): string {
14+
return path.join(__dirname, 'templates');
15+
}
16+
17+
/**
18+
* Get layouts directory path (resolved at runtime to avoid stale cached paths)
19+
*/
20+
private static getLayoutsDir(): string {
21+
return path.join(__dirname, 'templates', 'layouts');
22+
}
1123

1224
/**
1325
* Render an email template with the given variables
1426
*/
1527
static async render(options: TemplateRenderOptions): Promise<string> {
16-
const { template, variables, layout = 'base' } = options;
28+
const { template, variables, layout = 'base', logger } = options;
1729

1830
try {
1931
// Validate template exists
2032
const templatePath = this.getTemplatePath(template);
21-
if (!fs.existsSync(templatePath)) {
33+
const templatesDir = this.getTemplatesDir();
34+
const fileExists = fs.existsSync(templatePath);
35+
36+
// Structured trace logging with Pino logger
37+
if (logger) {
38+
logger.trace({
39+
operation: 'template_path_resolution',
40+
template,
41+
templatesDir,
42+
templatePath,
43+
fileExists,
44+
__dirname,
45+
cwd: process.cwd()
46+
}, `Resolving template path: ${template}`);
47+
}
48+
49+
if (!fileExists) {
2250
throw new Error(`Template '${template}' not found at ${templatePath}`);
2351
}
2452

@@ -33,7 +61,7 @@ export class TemplateRenderer {
3361
appName: 'DeployStack',
3462
// Layout information
3563
layout,
36-
layoutsDir: this.layoutsDir,
64+
layoutsDir: this.getLayoutsDir(),
3765
};
3866

3967
// Render the template
@@ -102,11 +130,12 @@ export class TemplateRenderer {
102130
*/
103131
static getAvailableTemplates(logger: FastifyBaseLogger): string[] {
104132
try {
105-
if (!fs.existsSync(this.templatesDir)) {
133+
const templatesDir = this.getTemplatesDir();
134+
if (!fs.existsSync(templatesDir)) {
106135
return [];
107136
}
108137

109-
return fs.readdirSync(this.templatesDir)
138+
return fs.readdirSync(templatesDir)
110139
.filter(file => file.endsWith('.pug') && !file.startsWith('_'))
111140
.map(file => file.replace('.pug', ''));
112141
} catch (error) {
@@ -139,9 +168,9 @@ export class TemplateRenderer {
139168
// Compile template
140169
const templatePath = this.getTemplatePath(template);
141170
const compiledTemplate = pug.compileFile(templatePath, {
142-
basedir: this.templatesDir,
171+
basedir: this.getTemplatesDir(),
143172
pretty: false,
144-
cache: true,
173+
cache: process.env.NODE_ENV === 'production', // Only cache in production to avoid stale paths
145174
});
146175

147176
// Cache the compiled template
@@ -154,19 +183,21 @@ export class TemplateRenderer {
154183
* Get the full path to a template file
155184
*/
156185
private static getTemplatePath(template: string): string {
157-
return path.join(this.templatesDir, `${template}.pug`);
186+
return path.join(this.getTemplatesDir(), `${template}.pug`);
158187
}
159188

160189
/**
161190
* Ensure templates directory exists
162191
*/
163192
static ensureTemplatesDirectory(): void {
164-
if (!fs.existsSync(this.templatesDir)) {
165-
fs.mkdirSync(this.templatesDir, { recursive: true });
193+
const templatesDir = this.getTemplatesDir();
194+
if (!fs.existsSync(templatesDir)) {
195+
fs.mkdirSync(templatesDir, { recursive: true });
166196
}
167197

168-
if (!fs.existsSync(this.layoutsDir)) {
169-
fs.mkdirSync(this.layoutsDir, { recursive: true });
198+
const layoutsDir = this.getLayoutsDir();
199+
if (!fs.existsSync(layoutsDir)) {
200+
fs.mkdirSync(layoutsDir, { recursive: true });
170201
}
171202
}
172203

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//- @description Email notification when a GitHub MCP server deployment fails
2+
//- @variables userName, serverName, repositoryUrl, branch, commitSha, deployedAt, installationUrl
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 GitHub Deployment Failed
7+
8+
p Hi #{userName},
9+
10+
p Unfortunately, your GitHub MCP server deployment encountered an error.
11+
12+
.deployment-info
13+
p
14+
strong Server Name:
15+
p= serverName
16+
17+
p
18+
strong Repository:
19+
p
20+
a(href=repositoryUrl)= repositoryUrl
21+
22+
p
23+
strong Branch:
24+
p= branch
25+
26+
p
27+
strong Commit:
28+
p= commitSha
29+
30+
p
31+
strong Attempted At:
32+
p= deployedAt
33+
34+
p Please check the installation details for more information about what went wrong.
35+
36+
if installationUrl
37+
.text-center
38+
a.button(href=installationUrl) View Installation Details
39+
40+
p.text-muted
41+
| Best regards,
42+
br
43+
| The DeployStack Team

0 commit comments

Comments
 (0)