nitrostack 1.0.0 → 1.0.1
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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/templates/typescript-auth/.env.example +23 -0
- package/templates/typescript-auth/src/app.module.ts +103 -0
- package/templates/typescript-auth/src/db/database.ts +163 -0
- package/templates/typescript-auth/src/db/seed.ts +374 -0
- package/templates/typescript-auth/src/db/setup.ts +87 -0
- package/templates/typescript-auth/src/events/analytics.service.ts +52 -0
- package/templates/typescript-auth/src/events/notification.service.ts +40 -0
- package/templates/typescript-auth/src/filters/global-exception.filter.ts +28 -0
- package/templates/typescript-auth/src/guards/README.md +75 -0
- package/templates/typescript-auth/src/guards/jwt.guard.ts +105 -0
- package/templates/typescript-auth/src/health/database.health.ts +41 -0
- package/templates/typescript-auth/src/index.ts +26 -0
- package/templates/typescript-auth/src/interceptors/transform.interceptor.ts +24 -0
- package/templates/typescript-auth/src/middleware/logging.middleware.ts +42 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.module.ts +16 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.prompts.ts +114 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.resources.ts +40 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +241 -0
- package/templates/typescript-auth/src/modules/auth/auth.module.ts +16 -0
- package/templates/typescript-auth/src/modules/auth/auth.prompts.ts +147 -0
- package/templates/typescript-auth/src/modules/auth/auth.resources.ts +84 -0
- package/templates/typescript-auth/src/modules/auth/auth.tools.ts +139 -0
- package/templates/typescript-auth/src/modules/cart/cart.module.ts +16 -0
- package/templates/typescript-auth/src/modules/cart/cart.prompts.ts +95 -0
- package/templates/typescript-auth/src/modules/cart/cart.resources.ts +44 -0
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/orders/orders.module.ts +16 -0
- package/templates/typescript-auth/src/modules/orders/orders.prompts.ts +88 -0
- package/templates/typescript-auth/src/modules/orders/orders.resources.ts +48 -0
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/products/products.module.ts +16 -0
- package/templates/typescript-auth/src/modules/products/products.prompts.ts +146 -0
- package/templates/typescript-auth/src/modules/products/products.resources.ts +98 -0
- package/templates/typescript-auth/src/modules/products/products.tools.ts +266 -0
- package/templates/typescript-auth/src/pipes/validation.pipe.ts +42 -0
- package/templates/typescript-auth/src/services/database.service.ts +90 -0
- package/templates/typescript-auth/src/widgets/app/add-to-cart/page.tsx +122 -0
- package/templates/typescript-auth/src/widgets/app/address-added/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/address-deleted/page.tsx +105 -0
- package/templates/typescript-auth/src/widgets/app/address-list/page.tsx +139 -0
- package/templates/typescript-auth/src/widgets/app/address-updated/page.tsx +153 -0
- package/templates/typescript-auth/src/widgets/app/cart-cleared/page.tsx +86 -0
- package/templates/typescript-auth/src/widgets/app/cart-updated/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/categories/page.tsx +134 -0
- package/templates/typescript-auth/src/widgets/app/layout.tsx +21 -0
- package/templates/typescript-auth/src/widgets/app/login-result/page.tsx +129 -0
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +206 -0
- package/templates/typescript-auth/src/widgets/app/order-details/page.tsx +225 -0
- package/templates/typescript-auth/src/widgets/app/order-history/page.tsx +218 -0
- package/templates/typescript-auth/src/widgets/app/product-card/page.tsx +121 -0
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +173 -0
- package/templates/typescript-auth/src/widgets/app/shopping-cart/page.tsx +187 -0
- package/templates/typescript-auth/src/widgets/app/whoami/page.tsx +165 -0
- package/templates/typescript-auth/src/widgets/next.config.js +38 -0
- package/templates/typescript-auth/src/widgets/package.json +18 -0
- package/templates/typescript-auth/src/widgets/styles/ecommerce.ts +169 -0
- package/templates/typescript-auth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth/src/widgets/types/tool-data.ts +141 -0
- package/templates/typescript-auth/src/widgets/widget-manifest.json +464 -0
- package/templates/typescript-auth/tsconfig.json +27 -0
- package/templates/typescript-auth-api-key/.env +15 -0
- package/templates/typescript-auth-api-key/.env.example +4 -0
- package/templates/typescript-auth-api-key/src/app.module.ts +38 -0
- package/templates/typescript-auth-api-key/src/guards/apikey.guard.ts +47 -0
- package/templates/typescript-auth-api-key/src/guards/multi-auth.guard.ts +157 -0
- package/templates/typescript-auth-api-key/src/health/system.health.ts +55 -0
- package/templates/typescript-auth-api-key/src/index.ts +47 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.module.ts +18 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.tools.ts +155 -0
- package/templates/typescript-auth-api-key/src/modules/demo/multi-auth.tools.ts +123 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-auth-api-key/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-auth-api-key/src/widgets/next.config.js +37 -0
- package/templates/typescript-auth-api-key/src/widgets/package.json +24 -0
- package/templates/typescript-auth-api-key/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth-api-key/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-auth-api-key/tsconfig.json +23 -0
- package/templates/typescript-oauth/.env.example +91 -0
- package/templates/typescript-oauth/src/app.module.ts +89 -0
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +127 -0
- package/templates/typescript-oauth/src/index.ts +74 -0
- package/templates/typescript-oauth/src/modules/demo/demo.module.ts +16 -0
- package/templates/typescript-oauth/src/modules/demo/demo.tools.ts +190 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-oauth/src/widgets/next.config.js +37 -0
- package/templates/typescript-oauth/src/widgets/package.json +24 -0
- package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-oauth/tsconfig.json +23 -0
- package/templates/typescript-starter/.env.example +4 -0
- package/templates/typescript-starter/src/app.module.ts +34 -0
- package/templates/typescript-starter/src/health/system.health.ts +55 -0
- package/templates/typescript-starter/src/index.ts +27 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-starter/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-starter/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-starter/src/widgets/next.config.js +37 -0
- package/templates/typescript-starter/src/widgets/package.json +24 -0
- package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-starter/tsconfig.json +23 -0
- package/LICENSE_URLS_UPDATE_COMPLETE.md +0 -388
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { McpApp, Module, OAuthModule } from 'nitrostack';
|
|
2
|
+
import { DemoModule } from './modules/demo/demo.module.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* App Module - Root module for the OAuth 2.1 MCP Server
|
|
6
|
+
*
|
|
7
|
+
* This module demonstrates OAuth 2.1 authentication according to:
|
|
8
|
+
* - MCP Specification: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
9
|
+
* - OpenAI Apps SDK Requirements: https://developers.openai.com/apps-sdk/build/auth
|
|
10
|
+
*
|
|
11
|
+
* OAuth 2.1 Standards Compliance:
|
|
12
|
+
* - OAuth 2.1 (draft-ietf-oauth-v2-1-13)
|
|
13
|
+
* - RFC 9728 - Protected Resource Metadata
|
|
14
|
+
* - RFC 8414 - Authorization Server Metadata
|
|
15
|
+
* - RFC 7591 - Dynamic Client Registration
|
|
16
|
+
* - RFC 8707 - Resource Indicators (Token Audience Binding)
|
|
17
|
+
* - RFC 7636 - PKCE
|
|
18
|
+
* - RFC 7662 - Token Introspection
|
|
19
|
+
*/
|
|
20
|
+
@McpApp({
|
|
21
|
+
module: AppModule,
|
|
22
|
+
server: {
|
|
23
|
+
name: 'OAuth 2.1 MCP Server',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
},
|
|
26
|
+
logging: {
|
|
27
|
+
level: 'info',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
@Module({
|
|
31
|
+
name: 'app',
|
|
32
|
+
description: 'Root application module with OAuth 2.1 authentication',
|
|
33
|
+
imports: [
|
|
34
|
+
// Enable OAuth 2.1 authentication
|
|
35
|
+
OAuthModule.forRoot({
|
|
36
|
+
// Resource URI - YOUR MCP server's public URL
|
|
37
|
+
// This is used for token audience binding (RFC 8707)
|
|
38
|
+
// CRITICAL: Tokens must be issued specifically for this URI
|
|
39
|
+
resourceUri: process.env.RESOURCE_URI || 'https://mcp.example.com',
|
|
40
|
+
|
|
41
|
+
// Authorization Server(s) - The OAuth provider URL(s)
|
|
42
|
+
// Supports multiple auth servers for federation scenarios
|
|
43
|
+
authorizationServers: [
|
|
44
|
+
process.env.AUTH_SERVER_URL || 'https://auth.example.com',
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
// Supported scopes for this MCP server
|
|
48
|
+
// Define what permissions your server supports
|
|
49
|
+
scopesSupported: [
|
|
50
|
+
'read', // Read access to resources
|
|
51
|
+
'write', // Write/modify resources
|
|
52
|
+
'admin', // Administrative operations
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
// Token Introspection (RFC 7662) - For opaque tokens
|
|
56
|
+
// If your OAuth provider issues opaque tokens (not JWTs),
|
|
57
|
+
// you MUST configure introspection to validate them
|
|
58
|
+
tokenIntrospectionEndpoint: process.env.INTROSPECTION_ENDPOINT,
|
|
59
|
+
tokenIntrospectionClientId: process.env.INTROSPECTION_CLIENT_ID,
|
|
60
|
+
tokenIntrospectionClientSecret: process.env.INTROSPECTION_CLIENT_SECRET,
|
|
61
|
+
|
|
62
|
+
// Expected audience (defaults to resourceUri if not provided)
|
|
63
|
+
// MUST match the audience claim in tokens (RFC 8707)
|
|
64
|
+
audience: process.env.TOKEN_AUDIENCE,
|
|
65
|
+
|
|
66
|
+
// Expected issuer (optional but recommended)
|
|
67
|
+
// If provided, tokens must be from this issuer
|
|
68
|
+
issuer: process.env.TOKEN_ISSUER,
|
|
69
|
+
|
|
70
|
+
// Custom validation (optional)
|
|
71
|
+
// Add any additional validation logic beyond spec requirements
|
|
72
|
+
customValidation: async (tokenPayload) => {
|
|
73
|
+
// Example: Check if user is active in your database
|
|
74
|
+
// const user = await db.users.findOne({ id: tokenPayload.sub });
|
|
75
|
+
// return user?.active === true;
|
|
76
|
+
|
|
77
|
+
// For demo, accept all valid tokens
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// Feature modules
|
|
83
|
+
DemoModule,
|
|
84
|
+
],
|
|
85
|
+
controllers: [],
|
|
86
|
+
providers: [],
|
|
87
|
+
})
|
|
88
|
+
export class AppModule {}
|
|
89
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Guard, ExecutionContext, OAuthModule, OAuthTokenPayload } from 'nitrostack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth Guard
|
|
5
|
+
*
|
|
6
|
+
* Validates OAuth 2.1 access tokens according to MCP specification.
|
|
7
|
+
*
|
|
8
|
+
* Performs:
|
|
9
|
+
* - Token validation (introspection or JWT validation)
|
|
10
|
+
* - Audience binding (RFC 8707) - CRITICAL for security
|
|
11
|
+
* - Scope validation
|
|
12
|
+
* - Expiration checking
|
|
13
|
+
*
|
|
14
|
+
* Compatible with:
|
|
15
|
+
* - OpenAI Apps SDK
|
|
16
|
+
* - Any RFC-compliant OAuth 2.1 provider
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* @Tool({
|
|
21
|
+
* name: 'protected_resource',
|
|
22
|
+
* description: 'A protected tool'
|
|
23
|
+
* })
|
|
24
|
+
* @UseGuards(OAuthGuard)
|
|
25
|
+
* async protectedTool() {
|
|
26
|
+
* // Only accessible with valid OAuth token
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class OAuthGuard implements Guard {
|
|
31
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
32
|
+
// Extract token from metadata (sent by Studio or OAuth client)
|
|
33
|
+
const authHeader = context.metadata?.authorization as string;
|
|
34
|
+
const metaToken = context.metadata?._oauth || context.metadata?.token;
|
|
35
|
+
|
|
36
|
+
let token: string | null = null;
|
|
37
|
+
|
|
38
|
+
// Try Bearer token format first
|
|
39
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
40
|
+
token = authHeader.substring(7);
|
|
41
|
+
} else if (metaToken) {
|
|
42
|
+
token = metaToken as string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!token) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'OAuth token required. Please authenticate in Studio (Auth → OAuth 2.1 tab) ' +
|
|
48
|
+
'or provide token in Authorization header: "Bearer <token>"'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate token using OAuthModule
|
|
53
|
+
const result = await OAuthModule.validateToken(token);
|
|
54
|
+
|
|
55
|
+
if (!result.valid) {
|
|
56
|
+
throw new Error(`OAuth token validation failed: ${result.error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract scopes from token payload
|
|
60
|
+
const payload = result.payload as OAuthTokenPayload;
|
|
61
|
+
const scopes = this.extractScopes(payload);
|
|
62
|
+
|
|
63
|
+
// Populate context.auth with OAuth token information
|
|
64
|
+
context.auth = {
|
|
65
|
+
subject: payload.sub,
|
|
66
|
+
scopes: scopes,
|
|
67
|
+
clientId: payload.client_id,
|
|
68
|
+
tokenPayload: payload,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract scopes from token payload
|
|
76
|
+
* Handles both "scope" (space-separated string) and "scopes" (array) formats
|
|
77
|
+
*/
|
|
78
|
+
private extractScopes(payload: OAuthTokenPayload): string[] {
|
|
79
|
+
if (payload.scopes && Array.isArray(payload.scopes)) {
|
|
80
|
+
return payload.scopes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (payload.scope && typeof payload.scope === 'string') {
|
|
84
|
+
return payload.scope.split(' ').filter(s => s.length > 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Scope Guard
|
|
93
|
+
*
|
|
94
|
+
* Validates that the OAuth token has required scopes.
|
|
95
|
+
* Use this in addition to OAuthGuard for fine-grained access control.
|
|
96
|
+
*
|
|
97
|
+
* Usage:
|
|
98
|
+
* ```typescript
|
|
99
|
+
* @Tool({ name: 'admin_action' })
|
|
100
|
+
* @UseGuards(OAuthGuard, createScopeGuard(['admin', 'write']))
|
|
101
|
+
* async adminAction() {
|
|
102
|
+
* // Requires OAuth token with 'admin' AND 'write' scopes
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function createScopeGuard(requiredScopes: string[]): new () => Guard {
|
|
107
|
+
return class ScopeGuard implements Guard {
|
|
108
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
109
|
+
const userScopes = context.auth?.scopes || [];
|
|
110
|
+
|
|
111
|
+
const missingScopes = requiredScopes.filter(
|
|
112
|
+
scope => !userScopes.includes(scope)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (missingScopes.length > 0) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Insufficient scope. Required: ${requiredScopes.join(', ')}. ` +
|
|
118
|
+
`Missing: ${missingScopes.join(', ')}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpApplicationFactory } from 'nitrostack';
|
|
3
|
+
import { AppModule } from './app.module.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OAuth 2.1 MCP Server
|
|
7
|
+
*
|
|
8
|
+
* Demonstrates OAuth 2.1 authentication for Model Context Protocol servers.
|
|
9
|
+
*
|
|
10
|
+
* Compliant with:
|
|
11
|
+
* - MCP Specification: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
12
|
+
* - OpenAI Apps SDK: https://developers.openai.com/apps-sdk/build/auth
|
|
13
|
+
*
|
|
14
|
+
* Standards:
|
|
15
|
+
* - OAuth 2.1 (draft-ietf-oauth-v2-1-13)
|
|
16
|
+
* - RFC 9728 - Protected Resource Metadata
|
|
17
|
+
* - RFC 8414 - Authorization Server Metadata
|
|
18
|
+
* - RFC 7591 - Dynamic Client Registration
|
|
19
|
+
* - RFC 8707 - Resource Indicators (Token Audience Binding)
|
|
20
|
+
* - RFC 7636 - PKCE
|
|
21
|
+
* - RFC 7662 - Token Introspection
|
|
22
|
+
*
|
|
23
|
+
* Compatible with:
|
|
24
|
+
* - Auth0
|
|
25
|
+
* - Okta
|
|
26
|
+
* - Keycloak
|
|
27
|
+
* - Any RFC-compliant OAuth 2.1 provider
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
async function bootstrap() {
|
|
31
|
+
try {
|
|
32
|
+
console.error('🔐 Starting OAuth 2.1 MCP Server...\n');
|
|
33
|
+
|
|
34
|
+
// Validate required environment variables
|
|
35
|
+
const requiredEnvVars = ['RESOURCE_URI', 'AUTH_SERVER_URL'];
|
|
36
|
+
const missing = requiredEnvVars.filter(v => !process.env[v]);
|
|
37
|
+
|
|
38
|
+
if (missing.length > 0) {
|
|
39
|
+
console.error('❌ Missing required environment variables:');
|
|
40
|
+
missing.forEach(v => console.error(` - ${v}`));
|
|
41
|
+
console.error('\n💡 Copy .env.example to .env and configure your OAuth provider');
|
|
42
|
+
console.error(' See OAUTH_SETUP.md for provider-specific setup guides\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create the MCP application
|
|
47
|
+
const app = await McpApplicationFactory.create(AppModule);
|
|
48
|
+
|
|
49
|
+
console.error('✅ OAuth 2.1 Module configured');
|
|
50
|
+
console.error(` Resource URI: ${process.env.RESOURCE_URI}`);
|
|
51
|
+
console.error(` Auth Server: ${process.env.AUTH_SERVER_URL}`);
|
|
52
|
+
console.error(` Scopes: read, write, admin`);
|
|
53
|
+
console.error(` Audience: ${process.env.TOKEN_AUDIENCE || process.env.RESOURCE_URI}\n`);
|
|
54
|
+
|
|
55
|
+
// Start the MCP server with HTTP transport (auto-configured by OAuthModule)
|
|
56
|
+
const transportType = (app as any)._transportType || 'stdio';
|
|
57
|
+
const transportOptions = (app as any)._transportOptions;
|
|
58
|
+
|
|
59
|
+
await app.start(transportType, transportOptions);
|
|
60
|
+
|
|
61
|
+
console.error('🚀 Server started successfully!');
|
|
62
|
+
console.error(' Open NitroStack Studio to test OAuth 2.1 flow');
|
|
63
|
+
console.error(' Configure OAuth in Studio → Auth → OAuth 2.1 tab\n');
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('❌ Failed to start server:', error);
|
|
67
|
+
console.error('\n💡 Check your OAuth configuration in .env');
|
|
68
|
+
console.error(' See OAUTH_SETUP.md for troubleshooting\n');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
bootstrap();
|
|
74
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Module } from 'nitrostack';
|
|
2
|
+
import { DemoTools } from './demo.tools.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Demo Module
|
|
6
|
+
*
|
|
7
|
+
* Contains example tools demonstrating OAuth 2.1 authentication
|
|
8
|
+
*/
|
|
9
|
+
@Module({
|
|
10
|
+
name: 'demo',
|
|
11
|
+
description: 'Demo module with OAuth 2.1 authentication examples',
|
|
12
|
+
controllers: [DemoTools],
|
|
13
|
+
})
|
|
14
|
+
export class DemoModule {}
|
|
15
|
+
|
|
16
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Injectable, ToolDecorator as Tool, UseGuards, z, ExecutionContext } from 'nitrostack';
|
|
2
|
+
import { OAuthGuard, createScopeGuard } from '../../guards/oauth.guard.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Demo Tools Module
|
|
6
|
+
*
|
|
7
|
+
* Demonstrates OAuth 2.1 authentication with:
|
|
8
|
+
* 1. Public tools (no authentication)
|
|
9
|
+
* 2. Protected tools (OAuth token required)
|
|
10
|
+
* 3. Scoped tools (specific permissions required)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class DemoTools {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PUBLIC TOOL - No authentication required
|
|
18
|
+
* Anyone can call this tool without OAuth
|
|
19
|
+
*/
|
|
20
|
+
@Tool({
|
|
21
|
+
name: 'get_server_info',
|
|
22
|
+
description: 'Get public server information (no authentication required)',
|
|
23
|
+
inputSchema: z.object({}),
|
|
24
|
+
})
|
|
25
|
+
async getServerInfo() {
|
|
26
|
+
return {
|
|
27
|
+
name: 'OAuth 2.1 MCP Server',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
description: 'Demonstrates OAuth 2.1 authentication for MCP',
|
|
30
|
+
authentication: {
|
|
31
|
+
type: 'OAuth 2.1',
|
|
32
|
+
compliant: [
|
|
33
|
+
'OAuth 2.1 (draft-ietf-oauth-v2-1-13)',
|
|
34
|
+
'RFC 9728 - Protected Resource Metadata',
|
|
35
|
+
'RFC 8707 - Resource Indicators (Token Audience Binding)',
|
|
36
|
+
'RFC 7636 - PKCE',
|
|
37
|
+
],
|
|
38
|
+
compatibleWith: ['OpenAI Apps SDK', 'Any RFC-compliant OAuth provider'],
|
|
39
|
+
},
|
|
40
|
+
availableTools: {
|
|
41
|
+
public: ['get_server_info'],
|
|
42
|
+
protected: ['get_user_profile', 'list_resources', 'create_resource'],
|
|
43
|
+
},
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* PROTECTED TOOL - OAuth token required
|
|
50
|
+
* Basic authentication, no specific scopes needed
|
|
51
|
+
*/
|
|
52
|
+
@Tool({
|
|
53
|
+
name: 'get_user_profile',
|
|
54
|
+
description: 'Get authenticated user profile (requires OAuth token)',
|
|
55
|
+
inputSchema: z.object({}),
|
|
56
|
+
})
|
|
57
|
+
@UseGuards(OAuthGuard)
|
|
58
|
+
async getUserProfile(args: {}, context?: ExecutionContext) {
|
|
59
|
+
const userId = context?.auth?.subject;
|
|
60
|
+
const scopes = context?.auth?.scopes || [];
|
|
61
|
+
const clientId = context?.auth?.clientId;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
message: 'User profile retrieved successfully',
|
|
65
|
+
user: {
|
|
66
|
+
id: userId,
|
|
67
|
+
authenticatedVia: 'OAuth 2.1',
|
|
68
|
+
scopes: scopes,
|
|
69
|
+
clientId: clientId,
|
|
70
|
+
},
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
note: 'This is a demo. In production, you would fetch real user data from your database.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SCOPED TOOL - Requires 'read' scope
|
|
78
|
+
* Demonstrates fine-grained access control
|
|
79
|
+
*/
|
|
80
|
+
@Tool({
|
|
81
|
+
name: 'list_resources',
|
|
82
|
+
description: 'List available resources (requires OAuth token with "read" scope)',
|
|
83
|
+
inputSchema: z.object({
|
|
84
|
+
category: z.enum(['documents', 'images', 'videos', 'all']).default('all').describe('Resource category'),
|
|
85
|
+
limit: z.number().min(1).max(100).default(10).describe('Maximum number of resources to return'),
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
@UseGuards(OAuthGuard, createScopeGuard(['read']))
|
|
89
|
+
async listResources(
|
|
90
|
+
args: { category: string; limit: number },
|
|
91
|
+
context?: ExecutionContext
|
|
92
|
+
) {
|
|
93
|
+
const userId = context?.auth?.subject;
|
|
94
|
+
|
|
95
|
+
// Generate demo resources
|
|
96
|
+
const resources = Array.from({ length: args.limit }, (_, i) => ({
|
|
97
|
+
id: `resource_${i + 1}`,
|
|
98
|
+
name: `${args.category === 'all' ? 'Sample' : args.category} Resource ${i + 1}`,
|
|
99
|
+
type: args.category === 'all' ? ['document', 'image', 'video'][i % 3] : args.category,
|
|
100
|
+
owner: userId,
|
|
101
|
+
createdAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
message: 'Resources listed successfully',
|
|
106
|
+
category: args.category,
|
|
107
|
+
count: resources.length,
|
|
108
|
+
resources: resources,
|
|
109
|
+
requiredScope: 'read',
|
|
110
|
+
authenticatedUser: userId,
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* SCOPED TOOL - Requires 'write' scope
|
|
117
|
+
* Demonstrates write operations with OAuth
|
|
118
|
+
*/
|
|
119
|
+
@Tool({
|
|
120
|
+
name: 'create_resource',
|
|
121
|
+
description: 'Create a new resource (requires OAuth token with "write" scope)',
|
|
122
|
+
inputSchema: z.object({
|
|
123
|
+
name: z.string().describe('Resource name'),
|
|
124
|
+
type: z.enum(['document', 'image', 'video']).describe('Resource type'),
|
|
125
|
+
content: z.string().optional().describe('Resource content or description'),
|
|
126
|
+
}),
|
|
127
|
+
})
|
|
128
|
+
@UseGuards(OAuthGuard, createScopeGuard(['write']))
|
|
129
|
+
async createResource(
|
|
130
|
+
args: { name: string; type: string; content?: string },
|
|
131
|
+
context?: ExecutionContext
|
|
132
|
+
) {
|
|
133
|
+
const userId = context?.auth?.subject;
|
|
134
|
+
const resourceId = `resource_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
message: 'Resource created successfully',
|
|
138
|
+
resource: {
|
|
139
|
+
id: resourceId,
|
|
140
|
+
name: args.name,
|
|
141
|
+
type: args.type,
|
|
142
|
+
content: args.content || 'No content provided',
|
|
143
|
+
owner: userId,
|
|
144
|
+
createdAt: new Date().toISOString(),
|
|
145
|
+
},
|
|
146
|
+
requiredScope: 'write',
|
|
147
|
+
authenticatedUser: userId,
|
|
148
|
+
note: 'This is a demo. In production, this would persist to your database.',
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* MULTI-SCOPE TOOL - Requires both 'read' and 'admin' scopes
|
|
155
|
+
* Demonstrates multiple scope requirements
|
|
156
|
+
*/
|
|
157
|
+
@Tool({
|
|
158
|
+
name: 'admin_statistics',
|
|
159
|
+
description: 'Get administrative statistics (requires OAuth token with "read" and "admin" scopes)',
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
timeRange: z.enum(['24h', '7d', '30d', '90d']).default('7d').describe('Time range for statistics'),
|
|
162
|
+
}),
|
|
163
|
+
})
|
|
164
|
+
@UseGuards(OAuthGuard, createScopeGuard(['read', 'admin']))
|
|
165
|
+
async getAdminStatistics(
|
|
166
|
+
args: { timeRange: string },
|
|
167
|
+
context?: ExecutionContext
|
|
168
|
+
) {
|
|
169
|
+
const userId = context?.auth?.subject;
|
|
170
|
+
const scopes = context?.auth?.scopes || [];
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
message: 'Admin statistics retrieved successfully',
|
|
174
|
+
timeRange: args.timeRange,
|
|
175
|
+
statistics: {
|
|
176
|
+
totalUsers: Math.floor(Math.random() * 10000),
|
|
177
|
+
totalResources: Math.floor(Math.random() * 50000),
|
|
178
|
+
apiCalls: Math.floor(Math.random() * 1000000),
|
|
179
|
+
activeTokens: Math.floor(Math.random() * 5000),
|
|
180
|
+
},
|
|
181
|
+
requiredScopes: ['read', 'admin'],
|
|
182
|
+
authenticatedUser: userId,
|
|
183
|
+
userScopes: scopes,
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
note: 'This tool requires elevated permissions. Only users with admin scope can access this data.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { withToolData } from 'nitrostack/widgets';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Widget Metadata (stored in widget-manifest.json)
|
|
7
|
+
*
|
|
8
|
+
* This widget lists all available calculator operations.
|
|
9
|
+
*
|
|
10
|
+
* Example includes all four operations: add, subtract, multiply, divide
|
|
11
|
+
*
|
|
12
|
+
* Frontend developers: Edit widget-manifest.json to update examples
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Operation {
|
|
16
|
+
name: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
description: string;
|
|
19
|
+
example: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OperationsData {
|
|
23
|
+
operations: Operation[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CalculatorOperations({ data }: { data: OperationsData }) {
|
|
27
|
+
const getOperationColor = (name: string) => {
|
|
28
|
+
const colors: Record<string, string> = {
|
|
29
|
+
add: '#10b981',
|
|
30
|
+
subtract: '#f59e0b',
|
|
31
|
+
multiply: '#3b82f6',
|
|
32
|
+
divide: '#8b5cf6'
|
|
33
|
+
};
|
|
34
|
+
return colors[name] || '#6b7280';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={{
|
|
39
|
+
padding: '24px',
|
|
40
|
+
background: 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
|
|
41
|
+
borderRadius: '16px',
|
|
42
|
+
maxWidth: '500px'
|
|
43
|
+
}}>
|
|
44
|
+
<h3 style={{
|
|
45
|
+
margin: '0 0 20px 0',
|
|
46
|
+
fontSize: '24px',
|
|
47
|
+
color: '#1f2937',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
gap: '12px'
|
|
51
|
+
}}>
|
|
52
|
+
<span style={{ fontSize: '32px' }}>🔢</span>
|
|
53
|
+
Calculator Operations
|
|
54
|
+
</h3>
|
|
55
|
+
|
|
56
|
+
<div style={{ display: 'grid', gap: '12px' }}>
|
|
57
|
+
{data.operations.map((op) => (
|
|
58
|
+
<div
|
|
59
|
+
key={op.name}
|
|
60
|
+
style={{
|
|
61
|
+
background: 'white',
|
|
62
|
+
borderRadius: '12px',
|
|
63
|
+
padding: '16px',
|
|
64
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
65
|
+
borderLeft: `4px solid ${getOperationColor(op.name)}`
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<div style={{
|
|
69
|
+
display: 'flex',
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
gap: '12px',
|
|
72
|
+
marginBottom: '8px'
|
|
73
|
+
}}>
|
|
74
|
+
<div style={{
|
|
75
|
+
width: '40px',
|
|
76
|
+
height: '40px',
|
|
77
|
+
borderRadius: '10px',
|
|
78
|
+
background: getOperationColor(op.name),
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
color: 'white',
|
|
83
|
+
fontSize: '24px',
|
|
84
|
+
fontWeight: 'bold'
|
|
85
|
+
}}>
|
|
86
|
+
{op.symbol}
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<div style={{
|
|
90
|
+
fontSize: '16px',
|
|
91
|
+
fontWeight: 'bold',
|
|
92
|
+
color: '#1f2937',
|
|
93
|
+
textTransform: 'capitalize'
|
|
94
|
+
}}>
|
|
95
|
+
{op.name}
|
|
96
|
+
</div>
|
|
97
|
+
<div style={{
|
|
98
|
+
fontSize: '14px',
|
|
99
|
+
color: '#6b7280'
|
|
100
|
+
}}>
|
|
101
|
+
{op.description}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div style={{
|
|
106
|
+
marginTop: '12px',
|
|
107
|
+
padding: '8px 12px',
|
|
108
|
+
background: '#f9fafb',
|
|
109
|
+
borderRadius: '8px',
|
|
110
|
+
fontFamily: 'monospace',
|
|
111
|
+
fontSize: '14px',
|
|
112
|
+
color: '#374151'
|
|
113
|
+
}}>
|
|
114
|
+
{op.example}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div style={{
|
|
121
|
+
marginTop: '16px',
|
|
122
|
+
textAlign: 'center',
|
|
123
|
+
fontSize: '12px',
|
|
124
|
+
color: '#6b7280'
|
|
125
|
+
}}>
|
|
126
|
+
✨ Use the 'calculate' tool to perform these operations
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default withToolData(CalculatorOperations);
|
|
133
|
+
|