This is page 1 of 2. Use http://codebase.md/ejb503/systemprompt-mcp-gmail?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .babelrc
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── config
│ ├── __llm__
│ │ └── README.md
│ └── server-config.ts
├── eslint.config.js
├── jest.config.mjs
├── jest.setup.ts
├── LICENSE.md
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── __mocks__
│ │ ├── @modelcontextprotocol
│ │ │ └── sdk.ts
│ │ ├── node_process.ts
│ │ ├── server.ts
│ │ └── systemprompt-service.ts
│ ├── __tests__
│ │ ├── index.test.ts
│ │ ├── mock-objects.ts
│ │ ├── server.test.ts
│ │ ├── test-utils.test.ts
│ │ └── test-utils.ts
│ ├── config
│ │ ├── __llm__
│ │ │ └── README.md
│ │ ├── __tests__
│ │ │ └── server-config.test.ts
│ │ └── server-config.ts
│ ├── constants
│ │ ├── instructions.ts
│ │ ├── message-handler.ts
│ │ ├── sampling-prompts.ts
│ │ └── tools.ts
│ ├── handlers
│ │ ├── __llm__
│ │ │ └── README.md
│ │ ├── __tests__
│ │ │ ├── callbacks.test.ts
│ │ │ ├── notifications.test.ts
│ │ │ ├── prompt-handlers.test.ts
│ │ │ ├── resource-handlers.test.ts
│ │ │ ├── sampling.test.ts
│ │ │ └── tool-handlers.test.ts
│ │ ├── callbacks.ts
│ │ ├── notifications.ts
│ │ ├── prompt-handlers.ts
│ │ ├── resource-handlers.ts
│ │ ├── sampling.ts
│ │ └── tool-handlers.ts
│ ├── index.ts
│ ├── schemas
│ │ └── generated
│ │ ├── index.ts
│ │ ├── SystempromptAgentRequestSchema.ts
│ │ ├── SystempromptBlockRequestSchema.ts
│ │ └── SystempromptPromptRequestSchema.ts
│ ├── server.ts
│ ├── services
│ │ ├── __llm__
│ │ │ └── README.md
│ │ ├── __tests__
│ │ │ ├── gmail-service.test.ts
│ │ │ ├── google-auth-service.test.ts
│ │ │ ├── google-base-service.test.ts
│ │ │ └── systemprompt-service.test.ts
│ │ ├── gmail-service.ts
│ │ ├── google-auth-service.ts
│ │ ├── google-base-service.ts
│ │ └── systemprompt-service.ts
│ ├── types
│ │ ├── __llm__
│ │ │ └── README.md
│ │ ├── gmail-types.ts
│ │ ├── index.ts
│ │ ├── sampling-schemas.ts
│ │ ├── sampling.ts
│ │ ├── systemprompt.ts
│ │ ├── tool-args.ts
│ │ └── tool-schemas.ts
│ └── utils
│ ├── __tests__
│ │ ├── mcp-mappers.test.ts
│ │ ├── message-handlers.test.ts
│ │ ├── tool-validation.test.ts
│ │ └── validation.test.ts
│ ├── mcp-mappers.ts
│ ├── message-handlers.ts
│ ├── tool-validation.ts
│ └── validation.ts
├── tsconfig.json
└── tsconfig.test.json
```
# Files
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "targets": { "node": "current" } }],
4 | "@babel/preset-typescript"
5 | ]
6 | }
7 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 |
6 | agent
7 | agent/*
8 |
9 | coverage/
10 | output/
11 | scripts/
12 |
13 | # Environment variables
14 | .env
15 |
16 | # Keep example file
17 | !.env.example
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": false,
5 | "printWidth": 100,
6 | "tabWidth": 2,
7 | "useTabs": false,
8 | "endOfLine": "auto"
9 | }
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Base64-encoded Google OAuth credentials (from Google Cloud Console)
2 | GOOGLE_CREDENTIALS=
3 |
4 | # Base64-encoded Google OAuth token (generated after authentication)
5 | GOOGLE_TOKEN=
```
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "env": {
3 | "node": true,
4 | "es2022": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
10 | "prettier"
11 | ],
12 | "parser": "@typescript-eslint/parser",
13 | "parserOptions": {
14 | "ecmaVersion": "latest",
15 | "sourceType": "module",
16 | "project": "./tsconfig.json"
17 | },
18 | "plugins": ["@typescript-eslint", "prettier"],
19 | "rules": {
20 | "prettier/prettier": "error",
21 | "@typescript-eslint/explicit-function-return-type": "error",
22 | "@typescript-eslint/no-explicit-any": "error",
23 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
24 | "no-console": ["warn", { "allow": ["warn", "error"] }]
25 | },
26 | "overrides": [
27 | {
28 | "files": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**/*.ts", "**/__mocks__/**/*.ts"],
29 | "rules": {
30 | "@typescript-eslint/no-explicit-any": "off",
31 | "@typescript-eslint/ban-ts-comment": "off",
32 | "@typescript-eslint/no-unsafe-assignment": "off",
33 | "@typescript-eslint/no-unsafe-member-access": "off",
34 | "@typescript-eslint/no-unsafe-call": "off",
35 | "@typescript-eslint/no-unsafe-return": "off"
36 | }
37 | }
38 | ]
39 | }
40 |
```
--------------------------------------------------------------------------------
/src/types/__llm__/README.md:
--------------------------------------------------------------------------------
```markdown
1 |
```
--------------------------------------------------------------------------------
/src/config/__llm__/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Configuration Directory Documentation
2 |
3 | ## Overview
4 |
5 | This directory contains the server configuration and metadata for the MCP server. It centralizes all configuration-related code to make it easy to modify server behavior and capabilities.
6 |
7 | ## Files
8 |
9 | ### `server-config.ts`
10 |
11 | The main configuration file that exports:
12 |
13 | - `serverConfig`: Server metadata and settings
14 | - `serverCapabilities`: Server capability definitions
15 |
16 | ## Configuration Structure
17 |
18 | ### Server Configuration
19 |
20 | ```typescript
21 | {
22 | name: string; // "systemprompt-agent-server"
23 | version: string; // Current server version
24 | metadata: {
25 | name: string; // "System Prompt Agent Server"
26 | description: string; // Server description
27 | icon: string; // "solar:align-horizontal-center-line-duotone"
28 | color: string; // "primary"
29 | serverStartTime: number; // Server start timestamp
30 | environment: string; // process.env.NODE_ENV
31 | customData: {
32 | serverFeatures: string[]; // ["agent", "prompts", "systemprompt"]
33 | }
34 | }
35 | }
36 | ```
37 |
38 | ### Server Capabilities
39 |
40 | ```typescript
41 | {
42 | capabilities: {
43 | resources: {
44 | listChanged: true, // Support for resource change notifications
45 | },
46 | tools: {}, // Tool-specific capabilities
47 | prompts: {
48 | listChanged: true, // Support for prompt change notifications
49 | }
50 | }
51 | }
52 | ```
53 |
54 | ## Usage
55 |
56 | Import the configuration objects from this directory when setting up the MCP server:
57 |
58 | ```typescript
59 | import { serverConfig, serverCapabilities } from "./config/server-config.js";
60 | ```
61 |
62 | ## Environment Variables
63 |
64 | The server uses the following environment variables:
65 |
66 | - `NODE_ENV`: Determines the runtime environment (development/production)
67 |
68 | ## Feature Flags
69 |
70 | The server supports the following features through `serverFeatures`:
71 |
72 | - `agent`: Agent management capabilities
73 | - `prompts`: Prompt creation and management
74 | - `systemprompt`: Integration with systemprompt.io
75 |
76 | ## Capabilities
77 |
78 | The server implements these MCP capabilities:
79 |
80 | - **Resources**: Supports change notifications for resource updates
81 | - **Prompts**: Supports change notifications for prompt updates
82 | - **Tools**: Extensible tool system (configuration determined at runtime)
83 |
```
--------------------------------------------------------------------------------
/config/__llm__/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # System Prompt Google Integration Server
2 |
3 | ## Overview
4 |
5 | This directory contains the configuration and metadata for the System Prompt Google Integration Server, which implements the Model Context Protocol (MCP) for Google services. It provides a standardized interface for AI agents to interact with Gmail and other Google APIs.
6 |
7 | ## Files
8 |
9 | ### `server-config.ts`
10 |
11 | The main configuration file that exports:
12 |
13 | - `serverConfig`: Server metadata and Google integration settings
14 | - `serverCapabilities`: Server capability definitions
15 |
16 | ## Configuration Structure
17 |
18 | ### Server Configuration
19 |
20 | ```typescript
21 | {
22 | name: string; // "systemprompt-mcp-google"
23 | version: string; // Current server version
24 | metadata: {
25 | name: string; // "System Prompt Google Integration Server"
26 | description: string; // Server description
27 | icon: string; // "mdi:google"
28 | color: string; // "blue"
29 | serverStartTime: number; // Server start timestamp
30 | environment: string; // process.env.NODE_ENV
31 | customData: {
32 | serverFeatures: string[]; // ["google-mail", "oauth2"]
33 | supportedAPIs: string[]; // ["gmail"]
34 | authProvider: string; // "google-oauth2"
35 | requiredScopes: string[]; // OAuth2 scopes needed for Google API access
36 | }
37 | }
38 | }
39 | ```
40 |
41 | ### Server Capabilities
42 |
43 | ```typescript
44 | {
45 | capabilities: {
46 | resources: {
47 | listChanged: true, // Support for resource change notifications
48 | },
49 | tools: {}, // Google API-specific tool capabilities
50 | prompts: {
51 | listChanged: true, // Support for prompt change notifications
52 | }
53 | }
54 | }
55 | ```
56 |
57 | ## Usage
58 |
59 | Import the configuration objects when setting up the MCP server:
60 |
61 | ```typescript
62 | import { serverConfig, serverCapabilities } from "./config/server-config.js";
63 | ```
64 |
65 | ## Environment Variables
66 |
67 | The server requires these environment variables:
68 |
69 | - `NODE_ENV`: Runtime environment (development/production)
70 | - `GOOGLE_CLIENT_ID`: OAuth2 client ID for Google API access
71 | - `GOOGLE_CLIENT_SECRET`: OAuth2 client secret
72 | - `GOOGLE_REDIRECT_URI`: OAuth2 redirect URI
73 |
74 | ## Features
75 |
76 | The server provides these core features:
77 |
78 | - **Gmail Integration**: Send, read, and manage emails
79 | - **OAuth2 Authentication**: Secure Google API access
80 | - **Resource Notifications**: Real-time updates for resource changes
81 | - **MCP Compliance**: Full implementation of the Model Context Protocol
82 |
83 | ## Supported Google APIs
84 |
85 | - Gmail API
86 | - Additional Google APIs can be added through configuration
87 |
88 | ## Authentication
89 |
90 | The server uses Google OAuth2 for authentication with the following scopes:
91 |
92 | - `https://www.googleapis.com/auth/gmail.modify`
93 |
94 | Additional scopes can be configured as needed for expanded API access.
95 |
```
--------------------------------------------------------------------------------
/src/services/__llm__/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Services Directory Documentation
2 |
3 | ## Overview
4 |
5 | This directory contains service implementations that handle external integrations and business logic for the MCP server. The services are organized into two main categories:
6 |
7 | 1. System Prompt Services - For interacting with the systemprompt.io API
8 | 2. Google Services - For interacting with various Google APIs (Gmail, Calendar, etc.)
9 |
10 | ## Service Architecture
11 |
12 | ### Base Services
13 |
14 | #### `google-base-service.ts`
15 |
16 | An abstract base class that provides common functionality for all Google services:
17 |
18 | ```typescript
19 | abstract class GoogleBaseService {
20 | protected auth: GoogleAuthService;
21 |
22 | constructor();
23 | protected waitForInit(): Promise<void>;
24 | }
25 | ```
26 |
27 | Features:
28 |
29 | - Automatic authentication initialization
30 | - Shared auth instance management
31 | - Error handling for auth failures
32 |
33 | ### Core Services
34 |
35 | #### `systemprompt-service.ts`
36 |
37 | A singleton service for interacting with the systemprompt.io API:
38 |
39 | ```typescript
40 | class SystemPromptService {
41 | private static instance: SystemPromptService | null = null;
42 |
43 | static initialize(apiKey: string, baseUrl?: string): void;
44 | static getInstance(): SystemPromptService;
45 | static cleanup(): void;
46 |
47 | // Prompt Operations
48 | async getAllPrompts(): Promise<SystempromptPromptResponse[]>;
49 |
50 | // Block Operations
51 | async listBlocks(): Promise<SystempromptBlockResponse[]>;
52 | async getBlock(blockId: string): Promise<SystempromptBlockResponse>;
53 | }
54 | ```
55 |
56 | Features:
57 |
58 | - Singleton pattern with API key initialization
59 | - Comprehensive error handling with specific error types
60 | - Configurable API endpoint
61 | - Type-safe request/response handling
62 |
63 | #### `google-auth-service.ts`
64 |
65 | Manages Google OAuth2 authentication:
66 |
67 | ```typescript
68 | class GoogleAuthService {
69 | static getInstance(): GoogleAuthService;
70 | async initialize(): Promise<void>;
71 | async authenticate(): Promise<void>;
72 | }
73 | ```
74 |
75 | #### `gmail-service.ts`
76 |
77 | Handles Gmail API interactions:
78 |
79 | ```typescript
80 | class GmailService extends GoogleBaseService {
81 | // Email operations
82 | async listMessages(): Promise<GmailMessage[]>;
83 | async sendEmail(): Promise<void>;
84 | // ... other Gmail operations
85 | }
86 | ```
87 |
88 | ## Implementation Details
89 |
90 | ### Error Handling
91 |
92 | All services implement comprehensive error handling:
93 |
94 | ```typescript
95 | try {
96 | const response = await fetch(endpoint, options);
97 | if (!response.ok) {
98 | switch (response.status) {
99 | case 403:
100 | throw new Error("Invalid API key");
101 | case 404:
102 | throw new Error("Resource not found");
103 | // ... other status codes
104 | }
105 | }
106 | } catch (error) {
107 | throw new Error(`API request failed: ${error.message}`);
108 | }
109 | ```
110 |
111 | ### Authentication
112 |
113 | #### System Prompt Authentication
114 |
115 | - API key-based authentication
116 | - Key passed via headers
117 | - Environment variable configuration
118 |
119 | #### Google Authentication
120 |
121 | - OAuth2 flow
122 | - Automatic token refresh
123 | - Scoped access for different services
124 |
125 | ## Usage Examples
126 |
127 | ### System Prompt Service
128 |
129 | ```typescript
130 | // Initialize
131 | SystemPromptService.initialize(process.env.SYSTEMPROMPT_API_KEY);
132 | const service = SystemPromptService.getInstance();
133 |
134 | // Get all prompts
135 | const prompts = await service.getAllPrompts();
136 |
137 | // List blocks
138 | const blocks = await service.listBlocks();
139 | ```
140 |
141 | ### Google Services
142 |
143 | ```typescript
144 | // Gmail
145 | const gmailService = new GmailService();
146 | await gmailService.waitForInit();
147 | const messages = await gmailService.listMessages();
148 | ```
149 |
150 | ## Testing
151 |
152 | All services have corresponding test files in the `__tests__` directory:
153 |
154 | - `systemprompt-service.test.ts`
155 | - `gmail-service.test.ts`
156 | - `google-auth-service.test.ts`
157 | - `google-base-service.test.ts`
158 |
159 | Tests cover:
160 |
161 | - Service initialization
162 | - API interactions
163 | - Error handling
164 | - Authentication flows
165 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # systemprompt-mcp-gmail
2 |
3 | [](https://www.npmjs.com/package/systemprompt-mcp-gmail)
4 | [](https://smithery.ai/server/systemprompt-mcp-gmail)
5 | [](https://opensource.org/licenses/Apache-2.0)
6 | [](https://twitter.com/tyingshoelaces_)
7 | [](https://discord.com/invite/wkAbSuPWpr)
8 |
9 | [Website](https://systemprompt.io) | [Documentation](https://systemprompt.io/documentation) | [Blog](https://tyingshoelaces.com) | [Get API Key](https://systemprompt.io/console)
10 |
11 | A specialized Model Context Protocol (MCP) server that enables you to search, read, delete and send emails from your Gmail account, leveraging an AI Agent to help with each operation. The server is designed to work with the [multimodal-mcp-client](https://github.com/Ejb503/multimodal-mcp-client), a voice-powered MCP client that provides the frontend interface.
12 |
13 | An API KEY is required to use this server. This is currently free, although this may change in the future. You can get one [here](https://systemprompt.io/console).
14 |
15 | This server uses Sampling and Notification functionality from the [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk).
16 |
17 | This will only work with advanced MCP clients that support these features.
18 |
19 | ## Required Client
20 |
21 | This server is designed to work with the [multimodal-mcp-client](https://github.com/Ejb503/multimodal-mcp-client) - a voice-powered MCP client that provides the frontend interface. Please make sure to set up both components for the full functionality.
22 |
23 | ## Why Use This Server?
24 |
25 | Send emails
26 | Search emails
27 | Read emails
28 |
29 | ## Installation
30 |
31 | This server requires GOOGLE_CREDENTIALS and GOOGLE_TOKEN environment variables to be set. These must be base64 encoded strings of the credentials and token. There is a script to help with this in the [multimodal-mcp-client](https://github.com/Ejb503/multimodal-mcp-client) repository. Follow the instructions here: https://github.com/Ejb503/multimodal-mcp-client/blob/master/scripts/google-auth/README.md
32 |
33 | After generating your base64 encoded strings, you can run the server with npx.
34 |
35 | ## Features
36 |
37 | #### Core Functionality
38 |
39 | - **MCP Protocol Integration**: Full implementation of Model Context Protocol for seamless AI agent interactions
40 | - **Voice-Powered Interface**: Compatible with voice commands through multimodal-mcp-client
41 | - **Real-Time Processing**: Supports streaming responses and real-time interactions
42 | - **Type-Safe Implementation**: Full TypeScript support with proper error handling
43 |
44 | #### Sampling & Notifications
45 |
46 | - Advanced sampling capabilities for AI responses
47 | - Real-time notification system for agent events
48 | - Configurable sampling parameters
49 | - Event-driven architecture for notifications
50 |
51 | #### Integration Features
52 |
53 | - API Key management and authentication
54 | - User status and billing information tracking
55 | - Subscription management
56 | - Usage monitoring and analytics
57 |
58 | ## 🎥 Demo & Showcase
59 |
60 | Watch our video demonstration to see Systemprompt MCP Gmail in action:
61 |
62 | [▶️ Watch Demo Video](https://www.youtube.com/watch?v=n94JtRXXqec)
63 |
64 | The demo showcases:
65 |
66 | - Voice-controlled AI interactions
67 | - Multimodal input processing
68 | - Tool execution and workflow automation
69 | - Real-time voice synthesis
70 |
71 | ## Installation
72 |
73 | ## Related Links
74 |
75 | - [Multimodal MCP Client](https://github.com/Ejb503/multimodal-mcp-client) - Voice-powered MCP client
76 | - [systemprompt.io Documentation](https://systemprompt.io/docs)
77 |
```
--------------------------------------------------------------------------------
/src/handlers/__llm__/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Handlers Directory Documentation
2 |
3 | ## Overview
4 |
5 | This directory contains the MCP server request handlers that implement core functionality for resources, tools, and prompts. The handlers integrate with Notion and systemprompt.io APIs to provide comprehensive page, database, and content management capabilities.
6 |
7 | ## Handler Files
8 |
9 | ### `resource-handlers.ts`
10 |
11 | Implements handlers for managing systemprompt.io blocks (resources):
12 |
13 | - `handleListResources()`: Lists available blocks with metadata
14 | - Currently returns the default agent resource
15 | - Includes name, description, and MIME type
16 | - `handleResourceCall()`: Retrieves block content by URI (`resource:///block/{id}`)
17 | - Validates URI format (`resource:///block/{id}`)
18 | - Returns block content with proper MCP formatting
19 | - Supports metadata and content management
20 |
21 | ### `tool-handlers.ts`
22 |
23 | Implements handlers for Notion operations and resource management tools:
24 |
25 | - `handleListTools()`: Lists available tools with their schemas
26 | - `handleToolCall()`: Executes tool operations:
27 |
28 | **Page Operations:**
29 |
30 | - `systemprompt_search_notion_pages`: Search pages with text queries
31 | - `systemprompt_get_notion_page`: Get specific page details
32 | - `systemprompt_create_notion_page`: Create new pages
33 | - `systemprompt_update_notion_page`: Update existing pages
34 |
35 | **Database Operations:**
36 |
37 | - `systemprompt_list_notion_databases`: List available databases
38 | - `systemprompt_get_database_items`: Query database items
39 |
40 | **Comment Operations:**
41 |
42 | - `systemprompt_create_notion_comment`: Create page comments
43 | - `systemprompt_get_notion_comments`: Get page comments
44 |
45 | **Resource Operations:**
46 |
47 | - `systemprompt_fetch_resource`: Retrieve block content
48 |
49 | ### `prompt-handlers.ts`
50 |
51 | Implements handlers for prompt management:
52 |
53 | - `handleListPrompts()`: Lists available prompts with metadata
54 | - Returns predefined prompts for common tasks
55 | - Includes name, description, and required arguments
56 | - `handleGetPrompt()`: Retrieves specific prompt by name
57 | - Returns prompt details with messages
58 | - Supports task-specific prompts (Page Manager, Database Organizer, etc.)
59 |
60 | ## Implementation Details
61 |
62 | ### Resource Handlers
63 |
64 | - Default agent resource provides core functionality
65 | - Includes specialized instructions for Notion operations
66 | - Supports voice configuration for audio responses
67 | - Returns content with proper MCP formatting
68 |
69 | ### Tool Handlers
70 |
71 | - Implements comprehensive Notion operations
72 | - Supports advanced page features:
73 | - Content searching and filtering
74 | - Property management
75 | - Page creation and updates
76 | - Hierarchical organization
77 | - Database integration features:
78 | - Database listing and exploration
79 | - Item querying and filtering
80 | - Content organization
81 | - Comment management:
82 | - Create and retrieve comments
83 | - Discussion thread support
84 | - Input validation through TypeScript interfaces
85 | - Proper error handling and response formatting
86 |
87 | ### Prompt Handlers
88 |
89 | - Predefined prompts for common tasks:
90 | - Notion Page Manager
91 | - Database Content Organizer
92 | - Page Commenter
93 | - Page Creator
94 | - Database Explorer
95 | - Each prompt includes:
96 | - Required and optional arguments
97 | - Clear descriptions
98 | - Task-specific instructions
99 | - Supports both static and dynamic instructions
100 |
101 | ## Error Handling
102 |
103 | - Comprehensive error handling across all handlers
104 | - Specific error cases:
105 | - Invalid resource URI format
106 | - Resource not found
107 | - Invalid tool parameters
108 | - API operation failures
109 | - Authentication errors
110 | - Descriptive error messages for debugging
111 | - Proper error propagation to clients
112 |
113 | ## Usage Example
114 |
115 | ```typescript
116 | // Register handlers with MCP server
117 | server.setRequestHandler(ListResourcesRequestSchema, handleListResources);
118 | server.setRequestHandler(ReadResourceRequestSchema, handleResourceCall);
119 | server.setRequestHandler(ListToolsRequestSchema, handleListTools);
120 | server.setRequestHandler(CallToolRequestSchema, handleToolCall);
121 | server.setRequestHandler(ListPromptsRequestSchema, handleListPrompts);
122 | server.setRequestHandler(GetPromptRequestSchema, handleGetPrompt);
123 | ```
124 |
125 | ## Notifications
126 |
127 | The server implements change notifications for:
128 |
129 | - Prompt updates (`sendPromptChangedNotification`)
130 | - Resource updates (`sendResourceChangedNotification`)
131 | - Tool operation completions
132 |
133 | These are sent asynchronously after successful operations to maintain responsiveness.
134 |
135 | ## Authentication
136 |
137 | - Integrates with Notion API for workspace access
138 | - Handles API token management
139 | - Supports required capabilities
140 | - Maintains secure credential handling
141 |
142 | ## Testing
143 |
144 | Comprehensive test coverage includes:
145 |
146 | - Unit tests for all handlers
147 | - Mock services for Notion operations
148 | - Error case validation
149 | - Response format verification
150 | - Tool operation validation
151 |
```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | Copyright (c) 2025 SystemPrompt
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License") with the following
8 | additional conditions:
9 |
10 | 1. You must obtain a valid API key from SystemPrompt (https://systemprompt.io/console)
11 | to use this software.
12 |
13 | 2. You may not use this software without a valid SystemPrompt API key.
14 |
15 | 3. You may not distribute, sublicense, or transfer your SystemPrompt API key
16 | to any third party.
17 |
18 | 4. SystemPrompt reserves the right to revoke API keys at any time.
19 |
20 | Subject to the foregoing conditions and the conditions of the Apache License,
21 | Version 2.0, you may obtain a copy of the License at
22 |
23 | http://www.apache.org/licenses/LICENSE-2.0
24 |
25 | Unless required by applicable law or agreed to in writing, software
26 | distributed under the License is distributed on an "AS IS" BASIS,
27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28 | See the License for the specific language governing permissions and
29 | limitations under the License.
30 |
31 | For licensing inquiries, please contact: [email protected]
32 |
```
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from "./tool-schemas.js";
2 | export * from "./tool-args.js";
3 | export * from "./systemprompt.js";
4 |
```
--------------------------------------------------------------------------------
/src/__mocks__/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 |
4 | export const mockServer: Partial<Server> = {};
5 |
6 | export const getMockServer = () => mockServer;
7 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { serverConfig, serverCapabilities } from "./config/server-config.js";
3 |
4 | export const server = new Server(serverConfig, serverCapabilities);
5 |
```
--------------------------------------------------------------------------------
/src/types/tool-args.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type ToolResponse = {
2 | content: {
3 | type: "text" | "resource";
4 | text: string;
5 | resource?: {
6 | uri: string;
7 | text: string;
8 | mimeType: string;
9 | };
10 | }[];
11 | _meta?: Record<string, unknown>;
12 | isError?: boolean;
13 | };
14 |
```
--------------------------------------------------------------------------------
/src/schemas/generated/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { SystempromptPromptRequestSchema } from "./SystempromptPromptRequestSchema.js";
2 | export { SystempromptBlockRequestSchema } from "./SystempromptBlockRequestSchema.js";
3 | export { SystempromptAgentRequestSchema } from "./SystempromptAgentRequestSchema.js";
4 |
```
--------------------------------------------------------------------------------
/src/__mocks__/node_process.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 |
3 | const mockProcess = {
4 | stdout: {
5 | write: jest.fn(),
6 | on: jest.fn(),
7 | },
8 | stdin: {
9 | on: jest.fn(),
10 | resume: jest.fn(),
11 | setEncoding: jest.fn(),
12 | },
13 | env: {},
14 | exit: jest.fn(),
15 | };
16 |
17 | export default mockProcess;
18 |
```
--------------------------------------------------------------------------------
/src/types/sampling.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
2 | import { Prompt } from "@modelcontextprotocol/sdk/types.js";
3 | import { JSONSchema7 } from "json-schema";
4 |
5 | /**
6 | * Represents a sampling prompt that extends the base MCP Prompt.
7 | * Used for generating sampling prompts and handling sampling operations.
8 | */
9 | export interface SamplingPrompt extends Prompt {
10 | messages: PromptMessage[];
11 | _meta: {
12 | complexResponseSchema?: JSONSchema7;
13 | responseSchema?: JSONSchema7;
14 | callback: string;
15 | };
16 | }
17 |
```
--------------------------------------------------------------------------------
/src/__tests__/server.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 | import { server } from "../server";
4 | import { serverConfig, serverCapabilities } from "../config/server-config";
5 |
6 | // Mock the Server class
7 | jest.mock("@modelcontextprotocol/sdk/server/index.js", () => ({
8 | Server: jest.fn(),
9 | }));
10 |
11 | describe("server", () => {
12 | it("should create server with correct config", () => {
13 | expect(Server).toHaveBeenCalledWith(serverConfig, serverCapabilities);
14 | expect(server).toBeDefined();
15 | });
16 | });
17 |
```
--------------------------------------------------------------------------------
/src/services/google-base-service.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { GoogleAuthService } from "./google-auth-service.js";
2 |
3 | export abstract class GoogleBaseService {
4 | protected auth = GoogleAuthService.getInstance();
5 | private initPromise: Promise<void>;
6 |
7 | constructor() {
8 | this.initPromise = this.initializeAuth();
9 | }
10 |
11 | private async initializeAuth(): Promise<void> {
12 | try {
13 | await this.auth.initialize();
14 | } catch (error) {
15 | console.error("Failed to initialize Google auth:", error);
16 | throw error;
17 | }
18 | }
19 |
20 | protected async waitForInit(): Promise<void> {
21 | await this.initPromise;
22 | }
23 | }
24 |
```
--------------------------------------------------------------------------------
/src/schemas/generated/SystempromptAgentRequestSchema.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { JSONSchema7 } from "json-schema";
2 |
3 | export const SystempromptAgentRequestSchema: JSONSchema7 = {
4 | type: "object",
5 | properties: {
6 | content: {
7 | type: "string",
8 | },
9 | metadata: {
10 | type: "object",
11 | properties: {
12 | title: {
13 | type: "string",
14 | },
15 | description: {
16 | type: ["null", "string"],
17 | },
18 | log_message: {
19 | type: "string",
20 | },
21 | },
22 | additionalProperties: false,
23 | },
24 | },
25 | additionalProperties: false,
26 | required: ["content", "metadata"],
27 | $schema: "http://json-schema.org/draft-07/schema#",
28 | };
29 |
```
--------------------------------------------------------------------------------
/src/handlers/notifications.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
2 | import { server } from "../server.js";
3 |
4 | export async function sendOperationNotification(operation: string, message: string): Promise<void> {
5 | const notification: ServerNotification = {
6 | method: "notifications/message",
7 | params: {
8 | _meta: {},
9 | message: `Operation ${operation}: ${message}`,
10 | level: "info",
11 | timestamp: new Date().toISOString(),
12 | },
13 | };
14 | await sendNotification(notification);
15 | }
16 |
17 | async function sendNotification(notification: ServerNotification) {
18 | await server.notification(notification);
19 | }
20 |
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["node", "jest", "@jest/globals"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "allowJs": true,
8 | "checkJs": false,
9 | "noImplicitAny": false,
10 | "strict": false,
11 | "strictNullChecks": false,
12 | "strictFunctionTypes": false,
13 | "noImplicitThis": false,
14 | "noUnusedLocals": false,
15 | "noUnusedParameters": false,
16 | "noImplicitReturns": false,
17 | "noFallthroughCasesInSwitch": false,
18 | "allowUnreachableCode": true,
19 | "skipLibCheck": true,
20 | "noStrictGenericChecks": true,
21 | "useUnknownInCatchVariables": false
22 | },
23 | "include": ["src/**/*", "**/*.test.ts", "**/*.test.tsx"],
24 | "exclude": ["node_modules"]
25 | }
26 |
```
--------------------------------------------------------------------------------
/src/types/gmail-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface EmailMetadata {
2 | id: string;
3 | threadId: string;
4 | snippet: string;
5 | from: {
6 | name?: string;
7 | email: string;
8 | };
9 | to: {
10 | name?: string;
11 | email: string;
12 | }[];
13 | subject: string;
14 | date: Date;
15 | labels: {
16 | id: string;
17 | name: string;
18 | }[];
19 | hasAttachments: boolean;
20 | isUnread: boolean;
21 | isImportant: boolean;
22 | }
23 |
24 | export interface SendEmailOptions {
25 | to: string | string[];
26 | subject: string;
27 | body: string;
28 | cc?: string | string[];
29 | bcc?: string | string[];
30 | replyTo?: string;
31 | attachments?: Array<{
32 | filename: string;
33 | content: Buffer | string;
34 | contentType?: string;
35 | }>;
36 | isHtml?: boolean;
37 | }
38 |
39 | export interface DraftEmailOptions extends SendEmailOptions {
40 | id?: string; // For updating existing drafts
41 | }
42 |
```
--------------------------------------------------------------------------------
/src/schemas/generated/SystempromptBlockRequestSchema.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { JSONSchema7 } from "json-schema";
2 |
3 | export const SystempromptBlockRequestSchema: JSONSchema7 = {
4 | type: "object",
5 | properties: {
6 | content: {
7 | type: "string",
8 | },
9 | prefix: {
10 | type: "string",
11 | description:
12 | "The prefix to use for the block. Must have no spaces or special characters.",
13 | },
14 | metadata: {
15 | type: "object",
16 | properties: {
17 | title: {
18 | type: "string",
19 | },
20 | description: {
21 | type: ["null", "string"],
22 | },
23 | log_message: {
24 | type: "string",
25 | },
26 | },
27 | additionalProperties: false,
28 | },
29 | },
30 | additionalProperties: false,
31 | required: ["content", "metadata", "prefix"],
32 | $schema: "http://json-schema.org/draft-07/schema#",
33 | };
34 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "allowJs": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "declaration": true,
16 | "sourceMap": true,
17 | "types": ["node", "jest"],
18 | "paths": {
19 | "@modelcontextprotocol/sdk": [
20 | "./node_modules/@modelcontextprotocol/sdk/dist/index.d.ts"
21 | ],
22 | "@modelcontextprotocol/sdk/*": [
23 | "./node_modules/@modelcontextprotocol/sdk/dist/*"
24 | ]
25 | }
26 | },
27 | "include": ["src/**/*"],
28 | "exclude": [
29 | "node_modules",
30 | "**/*.test.ts",
31 | "**/*.test.tsx",
32 | "**/__tests__/**",
33 | "**/__mocks__/**"
34 | ]
35 | }
36 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/notifications.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest, describe, it, expect, beforeEach } from "@jest/globals";
2 | import { sendOperationNotification } from "../notifications.js";
3 | import * as serverModule from "../../server.js";
4 |
5 | jest.mock("../../server.js", () => ({
6 | server: {
7 | notification: jest.fn().mockImplementation(() => Promise.resolve()),
8 | },
9 | }));
10 |
11 | describe("Notifications", () => {
12 | beforeEach(() => {
13 | jest.clearAllMocks();
14 | });
15 |
16 | describe("sendOperationNotification", () => {
17 | it("should send a notification with operation details", async () => {
18 | await sendOperationNotification("test", "Test message");
19 |
20 | expect(serverModule.server.notification).toHaveBeenCalledWith({
21 | method: "notifications/message",
22 | params: {
23 | _meta: {},
24 | message: "Operation test: Test message",
25 | level: "info",
26 | timestamp: expect.any(String),
27 | },
28 | });
29 | });
30 | });
31 | });
32 |
```
--------------------------------------------------------------------------------
/src/config/server-config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Implementation, ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
2 |
3 | export const serverConfig: Implementation = {
4 | name: "systemprompt-mcp-gmail",
5 | version: "1.0.10",
6 | metadata: {
7 | name: "System Prompt MCP Gmail",
8 | description:
9 | "A specialized Model Context Protocol (MCP) server that enables you to search, read, delete and send emails from your Gmail account, leveraging an AI Agent to help with each operation.",
10 | icon: "solar:align-horizontal-center-line-duotone",
11 | color: "primary",
12 | serverStartTime: Date.now(),
13 | environment: process.env.NODE_ENV,
14 | customData: {
15 | serverFeatures: ["agent", "prompts", "systemprompt"],
16 | },
17 | },
18 | };
19 |
20 | export const serverCapabilities: { capabilities: ServerCapabilities } = {
21 | capabilities: {
22 | resources: {
23 | listChanged: true,
24 | },
25 | tools: {},
26 | prompts: {
27 | listChanged: true,
28 | },
29 | sampling: {},
30 | logging: {},
31 | },
32 | };
33 |
```
--------------------------------------------------------------------------------
/src/utils/tool-validation.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { CallToolRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
2 | import { TOOLS } from "../constants/tools.js";
3 | import type { JSONSchema7 } from "json-schema";
4 | import { validateWithErrors } from "./validation.js";
5 |
6 | /**
7 | * Validates a tool request and returns the tool configuration if valid
8 | */
9 | export function validateToolRequest(request: CallToolRequest): Tool {
10 | if (!request.params?.name) {
11 | throw new Error("Invalid tool request: missing tool name");
12 | }
13 |
14 | const tool = TOOLS.find((t: Tool) => t.name === request.params.name);
15 | if (!tool) {
16 | throw new Error(`Unknown tool: ${request.params.name}`);
17 | }
18 |
19 | // Validate arguments against the tool's schema if present
20 | if (tool.inputSchema && request.params.arguments) {
21 | validateWithErrors(
22 | request.params.arguments,
23 | tool.inputSchema as JSONSchema7
24 | );
25 | }
26 |
27 | return tool;
28 | }
29 |
30 | /**
31 | * Gets the schema for a tool by name
32 | */
33 | export function getToolSchema(toolName: string): JSONSchema7 | undefined {
34 | const tool = TOOLS.find((t: Tool) => t.name === toolName);
35 | return tool?.inputSchema as JSONSchema7;
36 | }
37 |
```
--------------------------------------------------------------------------------
/config/server-config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Implementation, ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
2 |
3 | export const serverConfig: Implementation = {
4 | name: "systemprompt-mcp-gmail",
5 | version: "1.0.0",
6 | metadata: {
7 | name: "System Prompt Gmail Integration Server",
8 | description:
9 | "A specialized Model Context Protocol (MCP) server that enables you to search, read, delete and send emails from your Gmail account, leveraging an AI Agent to help with each operation. The server is designed to work with the [multimodal-mcp-client](https://github.com/Ejb503/multimodal-mcp-client), a voice-powered MCP client that provides the frontend interface.",
10 | icon: "solar:align-horizontal-center-line-duotone",
11 | color: "primary",
12 | serverStartTime: Date.now(),
13 | environment: process.env.NODE_ENV,
14 | customData: {
15 | serverFeatures: ["gmail", "agent", "google", "systemprompt"],
16 | },
17 | },
18 | };
19 |
20 | export const serverCapabilities: { capabilities: ServerCapabilities } = {
21 | capabilities: {
22 | resources: {
23 | listChanged: true,
24 | },
25 | tools: {},
26 | prompts: {
27 | listChanged: true,
28 | },
29 | sampling: {},
30 | logging: {},
31 | },
32 | };
33 |
```
--------------------------------------------------------------------------------
/src/schemas/generated/SystempromptPromptRequestSchema.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { JSONSchema7 } from "json-schema";
2 |
3 | export const SystempromptPromptRequestSchema: JSONSchema7 = {
4 | type: "object",
5 | properties: {
6 | metadata: {
7 | type: "object",
8 | properties: {
9 | title: {
10 | type: "string",
11 | description: "The title of the prompt",
12 | },
13 | description: {
14 | type: ["string"],
15 | description: "A detailed description of what the prompt does",
16 | },
17 | log_message: {
18 | type: "string",
19 | description: "A message to log when this prompt is used",
20 | },
21 | tag: {
22 | type: "array",
23 | items: {
24 | type: "string",
25 | },
26 | description: "Tags to categorize the prompt",
27 | },
28 | },
29 | additionalProperties: false,
30 | required: ["title", "description"],
31 | },
32 | instruction: {
33 | type: "object",
34 | properties: {
35 | static: {
36 | type: "string",
37 | description:
38 | "The static instruction text that defines the prompt behavior",
39 | },
40 | },
41 | additionalProperties: false,
42 | required: ["static"],
43 | },
44 | },
45 | additionalProperties: false,
46 | required: ["instruction", "metadata"],
47 | $schema: "http://json-schema.org/draft-07/schema#",
48 | };
49 |
```
--------------------------------------------------------------------------------
/jest.config.mjs:
--------------------------------------------------------------------------------
```
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | export default {
3 | preset: "ts-jest",
4 | testEnvironment: "node",
5 | extensionsToTreatAsEsm: [".ts", ".mts"],
6 | moduleNameMapper: {
7 | "(.+)\\.js": "$1",
8 | "^@modelcontextprotocol/sdk$": "<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts",
9 | "^@modelcontextprotocol/sdk/server/stdio$":
10 | "<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts",
11 | "^@modelcontextprotocol/sdk/server$": "<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts",
12 | "^node:process$": "<rootDir>/src/__mocks__/node_process.ts",
13 | },
14 | transform: {
15 | "^.+\\.ts$": [
16 | "ts-jest",
17 | {
18 | tsconfig: "tsconfig.json",
19 | useESM: true,
20 | },
21 | ],
22 | "^.+\\.js$": [
23 | "babel-jest",
24 | {
25 | presets: [["@babel/preset-env", { targets: { node: "current" } }]],
26 | },
27 | ],
28 | },
29 | transformIgnorePatterns: [],
30 | testMatch: ["**/__tests__/**/*.test.ts"],
31 | setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
32 | testTimeout: 10000,
33 | maxWorkers: 1, // Run tests sequentially
34 | detectOpenHandles: true,
35 | forceExit: true,
36 | clearMocks: true,
37 | resetMocks: true,
38 | restoreMocks: true,
39 | collectCoverage: true,
40 | collectCoverageFrom: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.d.ts", "!src/types/**/*"],
41 | };
42 |
```
--------------------------------------------------------------------------------
/src/__mocks__/@modelcontextprotocol/sdk.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import mockProcess from "../node_process";
3 |
4 | // Mock server type
5 | export type MockServer = {
6 | setRequestHandler: jest.Mock;
7 | connect: jest.Mock;
8 | onRequest: jest.Mock;
9 | };
10 |
11 | // Mock implementations
12 | const mockServer = jest.fn().mockImplementation(() => ({
13 | setRequestHandler: jest.fn(),
14 | connect: jest.fn(),
15 | onRequest: jest.fn(),
16 | }));
17 |
18 | const mockStdioServerTransport = jest.fn().mockImplementation(() => ({
19 | onRequest: jest.fn(),
20 | onNotification: jest.fn(),
21 | }));
22 |
23 | const mockTypes = {
24 | ListToolsRequest: jest.fn(),
25 | CallToolRequest: jest.fn(),
26 | ToolCallContent: jest.fn().mockImplementation((args: unknown) => ({
27 | type: "sampling_request",
28 | sampling_request: {
29 | method: "createPage",
30 | params: {
31 | parent: {
32 | type: "workspace",
33 | workspace: true,
34 | },
35 | properties: {
36 | title: [
37 | {
38 | text: {
39 | content: "Test Page",
40 | },
41 | },
42 | ],
43 | },
44 | children: [],
45 | },
46 | },
47 | })),
48 | };
49 |
50 | // Export everything needed by the tests
51 | export const Server = mockServer;
52 | export const StdioServerTransport = mockStdioServerTransport;
53 | export const types = mockTypes;
54 | export { mockProcess as process };
55 |
56 | // Default export for ESM compatibility
57 | export default {
58 | Server: mockServer,
59 | StdioServerTransport: mockStdioServerTransport,
60 | types: mockTypes,
61 | process: mockProcess,
62 | };
63 |
64 | // Mark as ESM module
65 | Object.defineProperty(exports, "__esModule", { value: true });
66 |
```
--------------------------------------------------------------------------------
/src/constants/message-handler.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const ERROR_MESSAGES = {
2 | INVALID_REQUEST: {
3 | MISSING_MESSAGES: "Invalid request: missing messages",
4 | MISSING_PARAMS: "Request must have params",
5 | EMPTY_MESSAGES: "Request must have at least one message",
6 | },
7 | VALIDATION: {
8 | INVALID_ROLE: 'Message role must be either "user" or "assistant"',
9 | MISSING_CONTENT: "Message must have a content object",
10 | INVALID_CONTENT_TYPE: 'Content type must be either "text" or "image"',
11 | INVALID_TEXT_CONTENT: "Text content must have a string text field",
12 | INVALID_IMAGE_DATA: "Image content must have a base64 data field",
13 | INVALID_IMAGE_MIME: "Image content must have a mimeType field",
14 | INVALID_MAX_TOKENS: "maxTokens must be a positive number",
15 | INVALID_TEMPERATURE: "temperature must be a number between 0 and 1",
16 | INVALID_INCLUDE_CONTEXT: 'includeContext must be "none", "thisServer", or "allServers"',
17 | INVALID_MODEL_PRIORITY: "Model preference priorities must be numbers between 0 and 1",
18 | },
19 | SAMPLING: {
20 | EXPECTED_TEXT: "Expected text response from LLM",
21 | UNKNOWN_CALLBACK: "Unknown callback type: ",
22 | REQUEST_FAILED: "Sampling request failed: ",
23 | },
24 | } as const;
25 |
26 | export const VALID_ROLES = ["user", "assistant"] as const;
27 | export const VALID_CONTENT_TYPES = ["text", "image", "resource"] as const;
28 | export const VALID_INCLUDE_CONTEXT = ["none", "thisServer", "allServers"] as const;
29 |
30 | export const XML_TAGS = {
31 | REQUEST_PARAMS_CLOSE: "</requestParams>",
32 | EXISTING_CONTENT_TEMPLATE: (content: string) => `</requestParams>
33 | <existingContent>
34 | ${content}
35 | </existingContent>`,
36 | } as const;
37 |
```
--------------------------------------------------------------------------------
/src/handlers/sampling.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { CreateMessageRequest, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js";
2 | import { validateRequest } from "../utils/validation.js";
3 | import { server } from "../server.js";
4 | import {
5 | handleSendEmailCallback,
6 | handleReplyEmailCallback,
7 | handleReplyDraftCallback,
8 | handleEditDraftCallback,
9 | } from "./callbacks.js";
10 |
11 | export async function sendSamplingRequest(
12 | request: CreateMessageRequest,
13 | ): Promise<CreateMessageResult> {
14 | try {
15 | validateRequest(request);
16 | const result = await server.createMessage(request.params);
17 |
18 | const callback = request.params._meta?.callback;
19 | if (callback && typeof callback === "string") {
20 | await handleCallback(callback, result);
21 | }
22 | return result;
23 | } catch (error) {
24 | console.error("Sampling request failed:", error instanceof Error ? error.message : error);
25 | if (error instanceof Error) {
26 | throw error;
27 | }
28 | throw new Error(`Failed to process sampling request: ${error || "Unknown error"}`);
29 | }
30 | }
31 |
32 | /**
33 | * Handles a callback based on its type
34 | * @param callback The callback type
35 | * @param result The LLM result
36 | * @returns The tool response
37 | */
38 | async function handleCallback(callback: string, result: CreateMessageResult): Promise<string> {
39 | switch (callback) {
40 | case "send_email":
41 | return handleSendEmailCallback(result);
42 | case "reply_email":
43 | return handleReplyEmailCallback(result);
44 | case "reply_draft":
45 | return handleReplyDraftCallback(result);
46 | case "edit_draft":
47 | return handleEditDraftCallback(result);
48 | default:
49 | throw new Error(`Unknown callback type: ${callback}`);
50 | }
51 | }
52 |
```
--------------------------------------------------------------------------------
/src/__tests__/test-utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 |
3 | /**
4 | * Type for mock response options
5 | */
6 | export interface MockResponseOptions {
7 | ok?: boolean;
8 | status?: number;
9 | statusText?: string;
10 | headers?: Record<string, string>;
11 | }
12 |
13 | /**
14 | * Creates a mock response object for testing
15 | */
16 | export function createMockResponse(
17 | data: any,
18 | options: MockResponseOptions = {}
19 | ): Response {
20 | const { ok = true, status = 200, statusText = "OK", headers = {} } = options;
21 |
22 | return {
23 | ok,
24 | status,
25 | statusText,
26 | headers: new Headers(headers),
27 | json: () => Promise.resolve(data),
28 | text: () =>
29 | Promise.resolve(typeof data === "string" ? data : JSON.stringify(data)),
30 | blob: () => Promise.resolve(new Blob()),
31 | } as Response;
32 | }
33 |
34 | /**
35 | * Test fixture generator for common test data
36 | */
37 | export class TestFixtures {
38 | static createNote(overrides = {}) {
39 | return {
40 | id: "test-note-1",
41 | title: "Test Note",
42 | content: "Test content",
43 | created: new Date().toISOString(),
44 | ...overrides,
45 | };
46 | }
47 |
48 | static createNoteList(count: number) {
49 | return Array.from({ length: count }, (_, i) =>
50 | this.createNote({ id: `test-note-${i + 1}` })
51 | );
52 | }
53 | }
54 |
55 | /**
56 | * Helper to wait for promises to resolve
57 | */
58 | export const flushPromises = () =>
59 | new Promise((resolve) => setImmediate(resolve));
60 |
61 | /**
62 | * Type guard for error objects
63 | */
64 | export function isError(error: unknown): error is Error {
65 | return error instanceof Error;
66 | }
67 |
68 | /**
69 | * Creates a partial mock object with type safety
70 | */
71 | export function createPartialMock<T extends object>(
72 | overrides: Partial<T> = {}
73 | ): jest.Mocked<T> {
74 | return overrides as jest.Mocked<T>;
75 | }
76 |
```
--------------------------------------------------------------------------------
/src/services/__tests__/google-base-service.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import { GoogleAuthService } from "../google-auth-service";
3 | import { GoogleBaseService } from "../google-base-service";
4 | import { OAuth2Client } from "google-auth-library";
5 |
6 | jest.mock("../google-auth-service");
7 |
8 | class TestGoogleService extends GoogleBaseService {
9 | constructor() {
10 | super();
11 | }
12 |
13 | public async testInit(): Promise<void> {
14 | await this.waitForInit();
15 | }
16 | }
17 |
18 | describe("GoogleBaseService", () => {
19 | let mockAuth: jest.Mocked<GoogleAuthService>;
20 | let service: TestGoogleService;
21 |
22 | beforeEach(() => {
23 | mockAuth = {
24 | initialize: jest.fn().mockImplementation(() => Promise.resolve()),
25 | authenticate: jest.fn().mockImplementation(() => Promise.resolve()),
26 | getAuth: jest.fn().mockReturnValue(new OAuth2Client()),
27 | saveToken: jest.fn().mockImplementation(() => Promise.resolve()),
28 | } as unknown as jest.Mocked<GoogleAuthService>;
29 |
30 | (GoogleAuthService.getInstance as jest.Mock).mockReturnValue(mockAuth);
31 | service = new TestGoogleService();
32 | });
33 |
34 | it("should initialize successfully", async () => {
35 | await expect(service.testInit()).resolves.not.toThrow();
36 | expect(mockAuth.initialize).toHaveBeenCalled();
37 | expect(mockAuth.authenticate).toHaveBeenCalled();
38 | });
39 |
40 | it("should handle initialization failure", async () => {
41 | mockAuth.initialize.mockRejectedValueOnce(new Error("Init failed"));
42 | await expect(service.testInit()).rejects.toThrow("Init failed");
43 | });
44 |
45 | it("should handle authentication failure", async () => {
46 | mockAuth.authenticate.mockRejectedValueOnce(new Error("Auth failed"));
47 | await expect(service.testInit()).rejects.toThrow("Auth failed");
48 | });
49 | });
50 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import eslint from "@eslint/js";
2 | import tseslint from "@typescript-eslint/eslint-plugin";
3 | import tsparser from "@typescript-eslint/parser";
4 | import globals from "globals";
5 |
6 | export default [
7 | eslint.configs.recommended,
8 | {
9 | ignores: [
10 | "build/**/*",
11 | "node_modules/**/*",
12 | "coverage/**/*",
13 | "jest.setup.ts",
14 | ],
15 | },
16 | {
17 | files: ["**/*.js", "**/*.ts", "**/*.tsx"],
18 | languageOptions: {
19 | parser: tsparser,
20 | parserOptions: {
21 | ecmaVersion: "latest",
22 | sourceType: "module",
23 | },
24 | globals: {
25 | ...globals.node,
26 | ...globals.jest,
27 | process: true,
28 | console: true,
29 | Buffer: true,
30 | fetch: true,
31 | Headers: true,
32 | Blob: true,
33 | setImmediate: true,
34 | RequestInfo: true,
35 | RequestInit: true,
36 | },
37 | },
38 | plugins: {
39 | "@typescript-eslint": tseslint,
40 | },
41 | rules: {
42 | ...tseslint.configs.recommended.rules,
43 | "@typescript-eslint/no-unused-vars": [
44 | "error",
45 | {
46 | argsIgnorePattern: "^_",
47 | varsIgnorePattern: "^_",
48 | },
49 | ],
50 | "@typescript-eslint/no-explicit-any": "off",
51 | "no-console": "off",
52 | "no-undef": "off",
53 | "no-dupe-keys": "off",
54 | },
55 | },
56 | {
57 | files: [
58 | "**/*.test.ts",
59 | "**/*.test.tsx",
60 | "**/__tests__/**/*.ts",
61 | "**/__mocks__/**/*.ts",
62 | "scripts/**/*",
63 | "src/handlers/**/*",
64 | "src/services/**/*",
65 | ],
66 | rules: {
67 | "@typescript-eslint/no-explicit-any": "off",
68 | "@typescript-eslint/ban-ts-comment": "off",
69 | "@typescript-eslint/no-unsafe-assignment": "off",
70 | "@typescript-eslint/no-unsafe-member-access": "off",
71 | "@typescript-eslint/no-unsafe-call": "off",
72 | "@typescript-eslint/no-unsafe-return": "off",
73 | "@typescript-eslint/no-unused-vars": "off",
74 | "no-unused-vars": "off",
75 | },
76 | },
77 | ];
78 |
```
--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import { main } from "../index";
3 | import { SystemPromptService } from "../services/systemprompt-service";
4 | import { server } from "../server";
5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6 |
7 | // Mock the server and transport
8 | jest.mock("../server", () => ({
9 | server: {
10 | setRequestHandler: jest.fn(),
11 | connect: jest.fn(),
12 | },
13 | }));
14 |
15 | // Create a mock transport class
16 | const mockTransport = {
17 | onRequest: jest.fn(),
18 | onNotification: jest.fn(),
19 | };
20 |
21 | jest.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
22 | StdioServerTransport: jest.fn().mockImplementation(() => mockTransport),
23 | }));
24 |
25 | // Mock the SystemPromptService
26 | jest.mock("../services/systemprompt-service", () => ({
27 | SystemPromptService: {
28 | initialize: jest.fn(),
29 | },
30 | }));
31 |
32 | describe("index", () => {
33 | const originalEnv = process.env;
34 |
35 | beforeEach(() => {
36 | jest.clearAllMocks();
37 | process.env = { ...originalEnv };
38 | process.env.SYSTEMPROMPT_API_KEY = "test-api-key";
39 | });
40 |
41 | afterEach(() => {
42 | process.env = originalEnv;
43 | });
44 |
45 | it("should initialize the server with API key", async () => {
46 | await main();
47 |
48 | expect(SystemPromptService.initialize).toHaveBeenCalledWith("test-api-key");
49 | });
50 |
51 | it("should throw error if API key is missing", async () => {
52 | delete process.env.SYSTEMPROMPT_API_KEY;
53 |
54 | await expect(main()).rejects.toThrow(
55 | "SYSTEMPROMPT_API_KEY environment variable is required"
56 | );
57 | });
58 |
59 | it("should set up request handlers", async () => {
60 | await main();
61 |
62 | // Verify that setRequestHandler was called for each handler
63 | expect(server.setRequestHandler).toHaveBeenCalledTimes(7);
64 | });
65 |
66 | it("should connect to transport", async () => {
67 | await main();
68 |
69 | expect(StdioServerTransport).toHaveBeenCalled();
70 | expect(server.connect).toHaveBeenCalled();
71 | const transport = (server.connect as jest.Mock).mock.calls[0][0];
72 | expect(transport).toBe(mockTransport);
73 | });
74 | });
75 |
```
--------------------------------------------------------------------------------
/src/types/tool-schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Tool argument schemas
2 | export interface ListEmailsArgs {
3 | maxResults?: number;
4 | after?: string; // Date in YYYY/MM/DD format
5 | before?: string; // Date in YYYY/MM/DD format
6 | sender?: string; // Sender email address
7 | to?: string; // Recipient email address
8 | subject?: string; // Subject line text
9 | hasAttachment?: boolean; // Whether the email has attachments
10 | label?: string; // Gmail label name
11 | }
12 |
13 | export interface GetEmailArgs {
14 | messageId: string;
15 | }
16 |
17 | export interface GetDraftArgs {
18 | draftId: string;
19 | }
20 |
21 | export interface SearchEmailsArgs {
22 | query: string;
23 | maxResults?: number;
24 | after?: string; // Date in YYYY/MM/DD format
25 | before?: string; // Date in YYYY/MM/DD format
26 | }
27 |
28 | export interface SendEmailAIArgs {
29 | to: string; // Recipient email address(es). Multiple addresses can be comma-separated.
30 | userInstructions: string; // Instructions for AI to generate the email
31 | replyTo?: string; // Optional message ID to reply to
32 | }
33 |
34 | export interface SendEmailManualArgs {
35 | to: string; // Recipient email address(es). Multiple addresses can be comma-separated.
36 | subject?: string; // Email subject line, optional if replyTo is provided
37 | body: string; // Email body content
38 | cc?: string; // CC recipient email address(es)
39 | bcc?: string; // BCC recipient email address(es)
40 | isHtml?: boolean; // Whether the body content is HTML
41 | replyTo?: string; // Optional message ID to reply to
42 | }
43 |
44 | export interface CreateDraftAIArgs {
45 | to: string; // Recipient email address(es). Multiple addresses can be comma-separated.
46 | userInstructions: string; // Instructions for AI to generate the draft
47 | replyTo?: string; // Optional message ID to reply to
48 | }
49 |
50 | export interface EditDraftAIArgs {
51 | draftId: string; // The ID of the draft to edit
52 | userInstructions: string; // Instructions for AI to edit the draft
53 | }
54 |
55 | export interface ListDraftsArgs {
56 | maxResults?: number;
57 | }
58 |
59 | export interface DeleteDraftArgs {
60 | draftId: string;
61 | }
62 |
63 | export interface TrashMessageArgs {
64 | messageId: string;
65 | }
66 |
67 | export interface ListLabelsArgs {}
68 |
```
--------------------------------------------------------------------------------
/src/handlers/prompt-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type {
2 | GetPromptRequest,
3 | GetPromptResult,
4 | ListPromptsRequest,
5 | ListPromptsResult,
6 | PromptMessage,
7 | } from "@modelcontextprotocol/sdk/types.js";
8 | import { PROMPTS } from "../constants/sampling-prompts.js";
9 | import { injectVariablesIntoText } from "../utils/message-handlers.js";
10 |
11 | export async function handleListPrompts(request: ListPromptsRequest): Promise<ListPromptsResult> {
12 | try {
13 | if (!PROMPTS || !Array.isArray(PROMPTS)) {
14 | throw new Error("Failed to fetch prompts");
15 | }
16 | return {
17 | prompts: PROMPTS.map(({ messages, ...rest }) => rest),
18 | };
19 | } catch (error: any) {
20 | console.error("Failed to fetch prompts:", error);
21 | throw error;
22 | }
23 | }
24 |
25 | export async function handleGetPrompt(request: GetPromptRequest): Promise<GetPromptResult> {
26 | try {
27 | if (!PROMPTS || !Array.isArray(PROMPTS)) {
28 | throw new Error("Failed to fetch prompts");
29 | }
30 |
31 | const foundPrompt = PROMPTS.find((p) => p.name === request.params.name);
32 | if (!foundPrompt) {
33 | throw new Error(`Prompt not found: ${request.params.name}`);
34 | }
35 |
36 | if (
37 | !foundPrompt.messages ||
38 | !Array.isArray(foundPrompt.messages) ||
39 | foundPrompt.messages.length === 0
40 | ) {
41 | throw new Error(`Messages not found for prompt: ${request.params.name}`);
42 | }
43 |
44 | const injectedMessages = foundPrompt.messages.map((message) => {
45 | if (message.role === "user" && message.content.type === "text" && request.params.arguments) {
46 | return {
47 | role: message.role,
48 | content: {
49 | type: "text" as const,
50 | text: injectVariablesIntoText(message.content.text, request.params.arguments),
51 | },
52 | } satisfies PromptMessage;
53 | }
54 | return message;
55 | });
56 |
57 | return {
58 | name: foundPrompt.name,
59 | description: foundPrompt.description,
60 | arguments: foundPrompt.arguments || [],
61 | messages: injectedMessages,
62 | _meta: foundPrompt._meta,
63 | };
64 | } catch (error: any) {
65 | console.error("Failed to fetch prompt:", error);
66 | throw error;
67 | }
68 | }
69 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "systemprompt-mcp-gmail",
3 | "version": "1.0.10",
4 | "description": "A specialized Model Context Protocol (MCP) server that enables you to search, read, delete and send emails from your Gmail account, leveraging an AI Agent to help with each operation.",
5 | "type": "module",
6 | "bin": {
7 | "systemprompt-mcp-gmail": "./build/index.js"
8 | },
9 | "files": [
10 | "build"
11 | ],
12 | "scripts": {
13 | "build": "tsc",
14 | "prepare": "npm run build",
15 | "watch": "tsc --watch",
16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:coverage": "jest --coverage",
20 | "auth-google": "node --loader ts-node/esm scripts/auth-google.ts"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/Ejb503/systemprompt-mcp-gmail"
25 | },
26 | "dependencies": {
27 | "@modelcontextprotocol/sdk": "^0.6.0",
28 | "ajv": "^8.17.1",
29 | "dotenv": "^16.4.7",
30 | "google-auth-library": "^9.15.0",
31 | "googleapis": "^144.0.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.24.0",
35 | "@babel/preset-env": "^7.24.0",
36 | "@babel/preset-typescript": "^7.24.0",
37 | "@types/dotenv": "^8.2.0",
38 | "@types/jest": "^29.5.12",
39 | "@types/json-schema": "^7.0.15",
40 | "@types/node": "^20.11.24",
41 | "@typescript-eslint/eslint-plugin": "^7.1.0",
42 | "@typescript-eslint/parser": "^7.1.0",
43 | "cross-env": "^7.0.3",
44 | "eslint": "^8.57.0",
45 | "eslint-config-prettier": "^9.1.0",
46 | "eslint-plugin-prettier": "^5.1.3",
47 | "jest": "^29.7.0",
48 | "prettier": "^3.2.5",
49 | "ts-jest": "^29.1.2",
50 | "ts-jest-resolver": "^2.0.1",
51 | "ts-node": "^10.9.2",
52 | "tsx": "^4.7.1",
53 | "typescript": "^5.3.3",
54 | "typescript-json-schema": "^0.63.0"
55 | },
56 | "keywords": [
57 | "systemprompt",
58 | "mcp",
59 | "model-context-protocol",
60 | "google",
61 | "gmail",
62 | "oauth"
63 | ],
64 | "author": "Ejb503",
65 | "license": "Apache-2.0",
66 | "bugs": {
67 | "url": "https://github.com/Ejb503/systemprompt-mcp-gmail/issues"
68 | },
69 | "homepage": "https://systemprompt.io",
70 | "engines": {
71 | "node": ">=18.0.0"
72 | }
73 | }
```
--------------------------------------------------------------------------------
/src/config/__tests__/server-config.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { serverConfig, serverCapabilities } from "../server-config";
2 | import type {
3 | Implementation,
4 | ServerCapabilities,
5 | } from "@modelcontextprotocol/sdk/types.js";
6 |
7 | describe("server-config", () => {
8 | describe("serverConfig", () => {
9 | it("should have correct implementation details", () => {
10 | const config = serverConfig as Implementation;
11 | expect(config.name).toBe("systemprompt-agent-server");
12 | expect(config.version).toBe("1.0.0");
13 | expect(config.metadata).toBeDefined();
14 | });
15 |
16 | it("should have correct metadata", () => {
17 | const config = serverConfig as Implementation;
18 | const metadata = config.metadata as {
19 | name: string;
20 | description: string;
21 | icon: string;
22 | color: string;
23 | serverStartTime: number;
24 | environment: string | undefined;
25 | customData: {
26 | serverFeatures: string[];
27 | };
28 | };
29 |
30 | expect(metadata.name).toBe("System Prompt Agent Server");
31 | expect(metadata.description).toBe(
32 | "A specialized MCP server for creating and managing systemprompt.io compatible prompts"
33 | );
34 | expect(metadata.icon).toBe("solar:align-horizontal-center-line-duotone");
35 | expect(metadata.color).toBe("primary");
36 | expect(metadata.serverStartTime).toBeDefined();
37 | expect(typeof metadata.serverStartTime).toBe("number");
38 | expect(metadata.environment).toBe(process.env.NODE_ENV);
39 | expect(metadata.customData).toEqual({
40 | serverFeatures: ["agent", "prompts", "systemprompt"],
41 | });
42 | });
43 | });
44 |
45 | describe("serverCapabilities", () => {
46 | it("should have correct capabilities structure", () => {
47 | const config = serverCapabilities as { capabilities: ServerCapabilities };
48 | expect(config.capabilities).toBeDefined();
49 | const { capabilities } = config;
50 |
51 | expect(capabilities.resources).toEqual({
52 | listChanged: true,
53 | });
54 |
55 | expect(capabilities.tools).toEqual({});
56 |
57 | expect(capabilities.prompts).toEqual({
58 | listChanged: true,
59 | });
60 |
61 | expect(capabilities.sampling).toEqual({});
62 | });
63 | });
64 | });
65 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { handleListResources, handleResourceCall } from "./handlers/resource-handlers.js";
4 | import { handleListTools, handleToolCall } from "./handlers/tool-handlers.js";
5 | import { handleListPrompts, handleGetPrompt } from "./handlers/prompt-handlers.js";
6 | import {
7 | ListResourcesRequestSchema,
8 | ReadResourceRequestSchema,
9 | ListToolsRequestSchema,
10 | ListPromptsRequestSchema,
11 | GetPromptRequestSchema,
12 | CallToolRequestSchema,
13 | CreateMessageRequestSchema,
14 | } from "@modelcontextprotocol/sdk/types.js";
15 | import { config } from "dotenv";
16 | import { SystemPromptService } from "./services/systemprompt-service.js";
17 | import { sendSamplingRequest } from "./handlers/sampling.js";
18 | import { server } from "./server.js";
19 | import { GoogleAuthService } from "./services/google-auth-service.js";
20 |
21 | export async function main() {
22 | config();
23 |
24 | const apiKey = process.env.SYSTEMPROMPT_API_KEY;
25 | if (!apiKey) {
26 | throw new Error("SYSTEMPROMPT_API_KEY environment variable is required");
27 | }
28 | const token = process.env.GOOGLE_TOKEN;
29 | const credentials = process.env.GOOGLE_CREDENTIALS;
30 | if (!token || !credentials) {
31 | throw new Error("GOOGLE_TOKEN and GOOGLE_CREDENTIALS environment variables are required");
32 | }
33 | SystemPromptService.initialize();
34 |
35 | // Initialize Google Auth
36 | const googleAuth = GoogleAuthService.getInstance();
37 | await googleAuth.initialize();
38 |
39 | server.setRequestHandler(ListResourcesRequestSchema, handleListResources);
40 | server.setRequestHandler(ReadResourceRequestSchema, handleResourceCall);
41 | server.setRequestHandler(ListToolsRequestSchema, handleListTools);
42 | server.setRequestHandler(CallToolRequestSchema, handleToolCall);
43 | server.setRequestHandler(ListPromptsRequestSchema, handleListPrompts);
44 | server.setRequestHandler(GetPromptRequestSchema, handleGetPrompt);
45 | server.setRequestHandler(CreateMessageRequestSchema, sendSamplingRequest);
46 |
47 | const transport = new StdioServerTransport();
48 | await server.connect(transport);
49 | }
50 |
51 | // Run the server unless in test environment
52 | if (process.env.NODE_ENV !== "test") {
53 | main().catch((error) => {
54 | console.error("Fatal error:", error);
55 | process.exit(1);
56 | });
57 | }
58 |
```
--------------------------------------------------------------------------------
/jest.setup.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import { createMockResponse, MockResponseOptions } from "./src/__tests__/test-utils";
3 |
4 | // Define global types for our custom matchers and utilities
5 | declare global {
6 | // eslint-disable-next-line @typescript-eslint/no-namespace
7 | namespace jest {
8 | interface Matchers<R> {
9 | toBeValidDate(): R;
10 | toBeValidUUID(): R;
11 | }
12 | }
13 |
14 | function mockFetchResponse(data: any, options?: MockResponseOptions): void;
15 | function mockFetchError(message: string): void;
16 | }
17 |
18 | // Mock fetch globally with a more flexible implementation
19 | const mockFetch = jest.fn((input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
20 | // Default success response
21 | return Promise.resolve(createMockResponse({}));
22 | });
23 |
24 | // Type assertion for global fetch mock
25 | global.fetch = mockFetch;
26 |
27 | // Utility to set up fetch mock responses
28 | global.mockFetchResponse = (data: any, options: MockResponseOptions = {}) => {
29 | mockFetch.mockImplementationOnce(() => Promise.resolve(createMockResponse(data, options)));
30 | };
31 |
32 | // Utility to set up fetch mock error
33 | global.mockFetchError = (message: string) => {
34 | mockFetch.mockImplementationOnce(() => Promise.reject(new Error(message)));
35 | };
36 |
37 | // Add custom matchers
38 | expect.extend({
39 | toBeValidDate(received: string) {
40 | const date = new Date(received);
41 | const pass = date instanceof Date && !isNaN(date.getTime());
42 | return {
43 | pass,
44 | message: () => `expected ${received} to ${pass ? "not " : ""}be a valid date string`,
45 | };
46 | },
47 | toBeValidUUID(received: string) {
48 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
49 | const pass = uuidRegex.test(received);
50 | return {
51 | pass,
52 | message: () => `expected ${received} to ${pass ? "not " : ""}be a valid UUID`,
53 | };
54 | },
55 | });
56 |
57 | // Increase the default timeout for all tests
58 | jest.setTimeout(10000);
59 |
60 | // Clear all mocks before each test
61 | beforeEach(() => {
62 | jest.clearAllMocks();
63 | });
64 |
65 | // Reset modules after each test
66 | afterEach(() => {
67 | jest.resetModules();
68 | });
69 |
70 | // Clean up any remaining handles after all tests
71 | afterAll(() => {
72 | jest.useRealTimers();
73 | });
74 |
75 | // Handle unhandled promise rejections
76 | process.on("unhandledRejection", (error) => {
77 | console.error("Unhandled Promise Rejection:", error);
78 | process.exit(1);
79 | });
80 |
```
--------------------------------------------------------------------------------
/src/utils/message-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
2 | import { XML_TAGS } from "../constants/message-handler.js";
3 |
4 | /**
5 | * Updates a user message with existing page content
6 | * @param messages Array of messages to update
7 | * @param blocks The page blocks to include
8 | */
9 | export function updateUserMessageWithContent(messages: PromptMessage[], blocks: unknown): void {
10 | const userMessageIndex = messages.findIndex((msg) => msg.role === "user");
11 | if (userMessageIndex === -1) return;
12 |
13 | const userMessage = messages[userMessageIndex];
14 | if (userMessage.content.type !== "text") return;
15 |
16 | messages[userMessageIndex] = {
17 | role: "user",
18 | content: {
19 | type: "text",
20 | text: userMessage.content.text.replace(
21 | XML_TAGS.REQUEST_PARAMS_CLOSE,
22 | XML_TAGS.EXISTING_CONTENT_TEMPLATE(JSON.stringify(blocks, null, 2)),
23 | ),
24 | },
25 | };
26 | }
27 |
28 | /**
29 | * Injects variables into text
30 | * @param text The text to inject variables into
31 | * @param variables The variables to inject
32 | * @returns The text with variables injected
33 | */
34 | export function injectVariablesIntoText(text: string, variables: Record<string, unknown>): string {
35 | // First handle conditional blocks
36 | text = text.replace(/{{#([^}]+)}}(.*?){{\/\1}}/gs, (_, key, content) => {
37 | return key in variables && variables[key] ? content : "";
38 | });
39 |
40 | // Then handle direct variable replacements
41 | const directMatches = text.match(/{{([^#/][^}]*)}}/g);
42 | if (!directMatches) return text;
43 |
44 | const missingVariables = directMatches
45 | .map((match) => match.slice(2, -2))
46 | .filter((key) => !(key in variables));
47 |
48 | if (missingVariables.length > 0) {
49 | throw new Error("Missing required variables: " + missingVariables.join(", "));
50 | }
51 |
52 | return text.replace(/{{([^#/][^}]*)}}/g, (_, key) => String(variables[key] ?? ""));
53 | }
54 |
55 | /**
56 | * Injects variables into a message
57 | * @param message The message to inject variables into
58 | * @param variables The variables to inject
59 | * @returns The message with variables injected
60 | */
61 | export function injectVariables(
62 | message: PromptMessage,
63 | variables: Record<string, unknown>,
64 | ): PromptMessage {
65 | if (message.content.type !== "text") return message;
66 |
67 | return {
68 | ...message,
69 | content: {
70 | type: "text",
71 | text: injectVariablesIntoText(message.content.text, variables),
72 | },
73 | };
74 | }
75 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/sampling.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest, describe, it, expect, beforeEach } from "@jest/globals";
2 | import type { CreateMessageRequest, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js";
3 | import { sendSamplingRequest } from "../sampling";
4 |
5 | // Mock server module
6 | const mockCreateMessage = jest.fn<(params: CreateMessageRequest) => Promise<CreateMessageResult>>();
7 | jest.mock("../../server", () => ({
8 | __esModule: true,
9 | server: {
10 | createMessage: mockCreateMessage,
11 | notification: jest.fn(),
12 | },
13 | }));
14 |
15 | // Mock all callback handlers
16 | const mockCallbacks = {
17 | handleSendEmailCallback: jest.fn<() => Promise<string>>(),
18 | };
19 |
20 | jest.mock("../callbacks", () => mockCallbacks);
21 |
22 | describe("sampling", () => {
23 | const mockResult: CreateMessageResult = {
24 | content: {
25 | type: "text",
26 | text: "Test response",
27 | },
28 | role: "assistant",
29 | model: "test-model",
30 | _meta: {},
31 | };
32 |
33 | const validRequest: CreateMessageRequest = {
34 | method: "sampling/createMessage",
35 | params: {
36 | messages: [
37 | {
38 | role: "user",
39 | content: {
40 | type: "text",
41 | text: "test message",
42 | },
43 | },
44 | ],
45 | maxTokens: 100,
46 | temperature: 0.7,
47 | includeContext: "none",
48 | _meta: {},
49 | },
50 | };
51 |
52 | beforeEach(() => {
53 | jest.clearAllMocks();
54 | mockCreateMessage.mockResolvedValue(mockResult);
55 | mockCallbacks.handleSendEmailCallback.mockResolvedValue("Email sent successfully");
56 | });
57 |
58 | describe("sendSamplingRequest", () => {
59 | it("should process sampling request successfully", async () => {
60 | const result = await sendSamplingRequest(validRequest);
61 | expect(result).toEqual(mockResult);
62 | expect(mockCreateMessage).toHaveBeenCalledWith(validRequest);
63 | });
64 |
65 | it("should handle errors gracefully", async () => {
66 | const error = new Error("Test error");
67 | mockCreateMessage.mockRejectedValueOnce(error);
68 | await expect(sendSamplingRequest(validRequest)).rejects.toThrow("Test error");
69 | });
70 |
71 | it("should validate request parameters", async () => {
72 | const invalidRequest = {
73 | ...validRequest,
74 | params: {
75 | ...validRequest.params,
76 | messages: [],
77 | },
78 | };
79 | await expect(sendSamplingRequest(invalidRequest)).rejects.toThrow(
80 | "Invalid request parameters",
81 | );
82 | });
83 | });
84 | });
85 |
```
--------------------------------------------------------------------------------
/src/handlers/resource-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | ReadResourceRequest,
3 | ListResourcesResult,
4 | ReadResourceResult,
5 | ListResourcesRequest,
6 | } from "@modelcontextprotocol/sdk/types.js";
7 |
8 | const AGENT_RESOURCE = {
9 | name: "Systemprompt default",
10 | description: "An expert agent for Gmail, Calendar management and task organization",
11 | instruction: `You are a specialized agent with deep expertise in email management, calendar organization, and task coordination. Your capabilities include:
12 |
13 | 1. Email Management (Gmail):
14 | - List, search, and analyze email messages
15 | - Send emails and manage drafts
16 | - Handle attachments and labels
17 | - Process email content intelligently
18 |
19 | 2. Calendar Operations:
20 | - Schedule and manage meetings
21 | - Analyze calendar availability
22 | - Track events and deadlines
23 | - Identify scheduling conflicts
24 |
25 | 3. Task Organization:
26 | - Create and manage todo lists
27 | - Prioritize tasks effectively
28 | - Track deadlines and progress
29 | - Optimize task workflows
30 |
31 | 4. Smart Communication:
32 | - Compose well-structured emails
33 | - Analyze communication patterns
34 | - Maintain professional tone
35 | - Handle multi-participant coordination
36 |
37 | You have access to specialized tools and prompts for each of these areas. Always select the most appropriate tool for the task and execute operations efficiently while maintaining high quality and reliability.`,
38 | };
39 |
40 | export async function handleListResources(
41 | request: ListResourcesRequest,
42 | ): Promise<ListResourcesResult> {
43 | return {
44 | resources: [
45 | {
46 | uri: "resource:///block/default",
47 | name: AGENT_RESOURCE.name,
48 | description: AGENT_RESOURCE.description,
49 | mimeType: "text/plain",
50 | },
51 | ],
52 | _meta: {},
53 | };
54 | }
55 |
56 | export async function handleResourceCall(
57 | request: ReadResourceRequest,
58 | ): Promise<ReadResourceResult> {
59 | const { uri } = request.params;
60 | const match = uri.match(/^resource:\/\/\/block\/(.+)$/);
61 |
62 | if (!match) {
63 | throw new Error("Invalid resource URI format - expected resource:///block/{id}");
64 | }
65 |
66 | const blockId = match[1];
67 | if (blockId !== "default") {
68 | throw new Error("Resource not found");
69 | }
70 |
71 | return {
72 | contents: [
73 | {
74 | uri: uri,
75 | mimeType: "text/plain",
76 | text: JSON.stringify({
77 | name: AGENT_RESOURCE.name,
78 | description: AGENT_RESOURCE.description,
79 | instruction: AGENT_RESOURCE.instruction,
80 | }),
81 | },
82 | ],
83 | _meta: { tag: ["agent"] },
84 | };
85 | }
86 |
```
--------------------------------------------------------------------------------
/src/services/google-auth-service.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { google } from "googleapis";
2 | import { OAuth2Client } from "google-auth-library";
3 |
4 | interface GoogleCredentials {
5 | web?: {
6 | client_id: string;
7 | client_secret: string;
8 | redirect_uris: string[];
9 | };
10 | installed?: {
11 | client_id: string;
12 | client_secret: string;
13 | redirect_uris: string[];
14 | };
15 | }
16 |
17 | export class GoogleAuthService {
18 | private static instance: GoogleAuthService;
19 | private oAuth2Client: OAuth2Client | null = null;
20 |
21 | private constructor() {}
22 |
23 | static getInstance(): GoogleAuthService {
24 | if (!GoogleAuthService.instance) {
25 | GoogleAuthService.instance = new GoogleAuthService();
26 | }
27 | return GoogleAuthService.instance;
28 | }
29 |
30 | async initialize(): Promise<void> {
31 | try {
32 | const credentialsBase64 = process.env.GOOGLE_CREDENTIALS;
33 | if (!credentialsBase64) {
34 | throw new Error("GOOGLE_CREDENTIALS environment variable is not set");
35 | }
36 |
37 | const decodedCredentials = Buffer.from(credentialsBase64, "base64").toString();
38 | const credentials: GoogleCredentials = JSON.parse(decodedCredentials);
39 |
40 | // Check credentials format first
41 | if (!credentials.web && !credentials.installed) {
42 | throw new Error(
43 | `Invalid credentials format: credentials must contain either 'web' or 'installed' configuration. Received keys: ${Object.keys(credentials).join(", ")}`,
44 | );
45 | }
46 |
47 | const config = credentials.web || credentials.installed;
48 | if (!config) {
49 | throw new Error("Neither web nor installed credentials found");
50 | }
51 |
52 | const { client_secret, client_id, redirect_uris } = config;
53 |
54 | if (!client_secret || !client_id || !redirect_uris?.length) {
55 | throw new Error(
56 | "Invalid credentials: missing required fields (client_secret, client_id, or redirect_uris)",
57 | );
58 | }
59 |
60 | this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
61 |
62 | const tokenBase64 = process.env.GOOGLE_TOKEN;
63 | try {
64 | if (tokenBase64) {
65 | const token = JSON.parse(Buffer.from(tokenBase64, "base64").toString());
66 | this.oAuth2Client.setCredentials(token);
67 | }
68 | } catch (error) {
69 | console.error("Error parsing token:", error);
70 | throw error;
71 | }
72 | } catch (error) {
73 | console.error("Error loading Google credentials:", error);
74 | throw error;
75 | }
76 | }
77 |
78 | getAuth(): OAuth2Client {
79 | if (!this.oAuth2Client) {
80 | throw new Error("OAuth2Client not initialized");
81 | }
82 | return this.oAuth2Client;
83 | }
84 | }
85 |
```
--------------------------------------------------------------------------------
/src/utils/__tests__/validation.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { validateRequest } from "../validation";
2 | import type { CreateMessageRequest } from "@modelcontextprotocol/sdk/types.js";
3 |
4 | describe("validateRequest", () => {
5 | const validRequest = {
6 | method: "sampling/createMessage",
7 | params: {
8 | messages: [
9 | {
10 | role: "user",
11 | content: {
12 | type: "text",
13 | text: "Hello world",
14 | },
15 | },
16 | ],
17 | maxTokens: 100,
18 | },
19 | } as CreateMessageRequest;
20 |
21 | it("should validate a correct request", () => {
22 | expect(() => validateRequest(validRequest)).not.toThrow();
23 | });
24 |
25 | it("should throw error for missing method", () => {
26 | const invalidRequest = {
27 | params: validRequest.params,
28 | };
29 | expect(() => validateRequest(invalidRequest)).toThrow();
30 | });
31 |
32 | it("should throw error for missing params", () => {
33 | const invalidRequest = {
34 | method: "sampling/createMessage",
35 | };
36 | expect(() => validateRequest(invalidRequest)).toThrow(
37 | "Request must have params"
38 | );
39 | });
40 |
41 | it("should throw error for empty messages array", () => {
42 | const invalidRequest = {
43 | ...validRequest,
44 | params: {
45 | ...validRequest.params,
46 | messages: [],
47 | },
48 | };
49 | expect(() => validateRequest(invalidRequest)).toThrow(
50 | "Request must have at least one message"
51 | );
52 | });
53 |
54 | it("should throw error for invalid message role", () => {
55 | const invalidRequest = {
56 | ...validRequest,
57 | params: {
58 | messages: [
59 | {
60 | role: "invalid",
61 | content: {
62 | type: "text",
63 | text: "Hello",
64 | },
65 | },
66 | ],
67 | },
68 | };
69 | expect(() => validateRequest(invalidRequest)).toThrow(
70 | 'Message role must be either "user" or "assistant"'
71 | );
72 | });
73 |
74 | it("should throw error for invalid content type", () => {
75 | const invalidRequest = {
76 | ...validRequest,
77 | params: {
78 | messages: [
79 | {
80 | role: "user",
81 | content: {
82 | type: "invalid",
83 | text: "Hello",
84 | },
85 | },
86 | ],
87 | },
88 | };
89 | expect(() => validateRequest(invalidRequest)).toThrow(
90 | 'Content type must be either "text" or "image"'
91 | );
92 | });
93 |
94 | it("should validate image content correctly", () => {
95 | const imageRequest = {
96 | ...validRequest,
97 | params: {
98 | messages: [
99 | {
100 | role: "user",
101 | content: {
102 | type: "image",
103 | data: "base64data",
104 | mimeType: "image/png",
105 | },
106 | },
107 | ],
108 | },
109 | };
110 | expect(() => validateRequest(imageRequest)).not.toThrow();
111 | });
112 | });
113 |
```
--------------------------------------------------------------------------------
/src/handlers/callbacks.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CreateMessageResult } from "@modelcontextprotocol/sdk/types.js";
2 | import { sendOperationNotification } from "./notifications.js";
3 | import { GmailService } from "../services/gmail-service.js";
4 |
5 | /**
6 | * Handles sending an email via Gmail API
7 | * @param result The LLM result
8 | * @returns The tool response
9 | */
10 | export async function handleSendEmailCallback(result: CreateMessageResult): Promise<string> {
11 | if (result.content.type !== "text") {
12 | throw new Error("Expected text content");
13 | }
14 |
15 | const emailRequest = JSON.parse(result.content.text);
16 | const gmail = new GmailService();
17 | const messageId = await gmail.sendEmail(emailRequest);
18 |
19 | const message = `Successfully sent email with id: ${messageId}`;
20 | await sendOperationNotification("send_email", message);
21 | return message;
22 | }
23 |
24 | /**
25 | * Handles replying to an email via Gmail API
26 | * @param result The LLM result
27 | * @returns The tool response
28 | */
29 | export async function handleReplyEmailCallback(result: CreateMessageResult): Promise<string> {
30 | if (result.content.type !== "text") {
31 | throw new Error("Expected text content");
32 | }
33 |
34 | const emailRequest = JSON.parse(result.content.text);
35 | const gmail = new GmailService();
36 | const messageId = await gmail.replyEmail(
37 | emailRequest.replyTo,
38 | emailRequest.body,
39 | emailRequest.isHtml,
40 | );
41 |
42 | const message = `Successfully sent reply with id: ${messageId}`;
43 | await sendOperationNotification("reply_email", message);
44 | return message;
45 | }
46 |
47 | /**
48 | * Handles creating a draft reply via Gmail API
49 | * @param result The LLM result
50 | * @returns The tool response
51 | */
52 | export async function handleReplyDraftCallback(result: CreateMessageResult): Promise<string> {
53 | if (result.content.type !== "text") {
54 | throw new Error("Expected text content");
55 | }
56 |
57 | const emailRequest = JSON.parse(result.content.text);
58 | const gmail = new GmailService();
59 | const draftId = await gmail.createDraft({
60 | ...emailRequest,
61 | replyTo: emailRequest.replyTo,
62 | });
63 |
64 | const message = `Successfully created draft reply with id: ${draftId}`;
65 | await sendOperationNotification("reply_draft", message);
66 | return message;
67 | }
68 |
69 | /**
70 | * Handles editing a draft via Gmail API
71 | * @param result The LLM result
72 | * @returns The tool response
73 | */
74 | export async function handleEditDraftCallback(result: CreateMessageResult): Promise<string> {
75 | if (result.content.type !== "text") {
76 | throw new Error("Expected text content");
77 | }
78 |
79 | const emailRequest = JSON.parse(result.content.text);
80 | const gmail = new GmailService();
81 | const draftId = await gmail.updateDraft({
82 | ...emailRequest,
83 | id: emailRequest.draftId,
84 | });
85 |
86 | const message = `Successfully updated draft with id: ${draftId}`;
87 | await sendOperationNotification("edit_draft", message);
88 | return message;
89 | }
90 |
```
--------------------------------------------------------------------------------
/src/services/systemprompt-service.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type {
2 | SystempromptPromptResponse,
3 | SystempromptBlockResponse,
4 | SystempromptAgentResponse,
5 | SystempromptUserStatusResponse,
6 | } from "../types/systemprompt.js";
7 |
8 | export class SystemPromptService {
9 | private static instance: SystemPromptService | null = null;
10 | private baseUrl: string;
11 |
12 | private constructor() {
13 | this.baseUrl = "https://api.systemprompt.io/v1";
14 | }
15 |
16 | public static initialize(): void {
17 | if (!SystemPromptService.instance) {
18 | SystemPromptService.instance = new SystemPromptService();
19 | }
20 | }
21 |
22 | public static getInstance(): SystemPromptService {
23 | if (!SystemPromptService.instance) {
24 | throw new Error("SystemPromptService must be initialized first");
25 | }
26 | return SystemPromptService.instance;
27 | }
28 |
29 | public static cleanup(): void {
30 | SystemPromptService.instance = null;
31 | }
32 |
33 | private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
34 | try {
35 | const url = `${this.baseUrl}${path}`;
36 | const response = await fetch(url, {
37 | method,
38 | headers: {
39 | "Content-Type": "application/json",
40 | "api-key": process.env.SYSTEMPROMPT_API_KEY as string,
41 | },
42 | body: body ? JSON.stringify(body) : undefined,
43 | });
44 |
45 | const text = await response.text();
46 | let data;
47 | try {
48 | data = text ? JSON.parse(text) : undefined;
49 | } catch (error) {
50 | throw new Error("Failed to parse API response");
51 | }
52 |
53 | if (!response.ok) {
54 | switch (response.status) {
55 | case 403:
56 | throw new Error("Invalid API key");
57 | case 404:
58 | throw new Error("Resource not found - it may have been deleted");
59 | default:
60 | throw new Error(data?.message || "API request failed");
61 | }
62 | }
63 |
64 | return data;
65 | } catch (error) {
66 | if (error instanceof Error) {
67 | throw error;
68 | }
69 | throw new Error("Failed to make API request");
70 | }
71 | }
72 |
73 | public async getAllPrompts(): Promise<SystempromptPromptResponse[]> {
74 | return this.request<SystempromptPromptResponse[]>("GET", "/prompts");
75 | }
76 |
77 | public async listBlocks(): Promise<SystempromptBlockResponse[]> {
78 | return this.request<SystempromptBlockResponse[]>("GET", "/blocks");
79 | }
80 |
81 | public async listAgents(): Promise<SystempromptAgentResponse[]> {
82 | return this.request<SystempromptAgentResponse[]>("GET", "/agents");
83 | }
84 |
85 | public async fetchUserStatus(): Promise<SystempromptUserStatusResponse> {
86 | return this.request<SystempromptUserStatusResponse>("GET", "/user/status");
87 | }
88 |
89 | public async deletePrompt(id: string): Promise<void> {
90 | await this.request<void>("DELETE", `/prompts/${id}`);
91 | }
92 |
93 | public async deleteBlock(id: string): Promise<void> {
94 | await this.request<void>("DELETE", `/blocks/${id}`);
95 | }
96 | }
97 |
```
--------------------------------------------------------------------------------
/src/types/systemprompt.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { JSONSchema7 } from "json-schema";
2 |
3 | export interface SystempromptBlockRequest {
4 | content: string;
5 | prefix: string;
6 | metadata: Partial<Metadata>;
7 | }
8 |
9 | export interface SystempromptAgentRequest {
10 | content: string;
11 | metadata: Partial<Metadata>;
12 | }
13 |
14 | export interface SystempromptBlockResponse {
15 | id: string;
16 | content: string;
17 | prefix: string;
18 | metadata: Metadata;
19 | _link?: string;
20 | }
21 |
22 | export interface SystempromptAgentResponse {
23 | id: string;
24 | content: string;
25 | prefix: string;
26 | metadata: Metadata;
27 | _link?: string;
28 | }
29 |
30 | export interface SystempromptPromptRequest {
31 | metadata: Partial<Metadata>;
32 | instruction: {
33 | static: string;
34 | };
35 | }
36 |
37 | export interface SystempromptUserStatusResponse {
38 | user: {
39 | id: string;
40 | uuid: string;
41 | name: string;
42 | email: string;
43 | roles: string[];
44 | paddle_id: string;
45 | };
46 | content: {
47 | prompt: number;
48 | artifact: number;
49 | block: number;
50 | conversation: number;
51 | };
52 | usage: {
53 | ai: {
54 | execution: number;
55 | token: number;
56 | };
57 | api: {
58 | generation: number;
59 | };
60 | };
61 | billing: {
62 | customer: {
63 | id: string;
64 | name: string | null;
65 | email: string;
66 | marketingConsent: boolean;
67 | status: string;
68 | customData: any;
69 | locale: string;
70 | createdAt: {
71 | date: string;
72 | timezone_type: number;
73 | timezone: string;
74 | };
75 | updatedAt: {
76 | date: string;
77 | timezone_type: number;
78 | timezone: string;
79 | };
80 | importMeta: any;
81 | };
82 | subscription: Array<{
83 | id: string;
84 | status: string;
85 | currency_code: string;
86 | billing_cycle: {
87 | frequency: number;
88 | interval: string;
89 | };
90 | current_billing_period: {
91 | starts_at: string;
92 | ends_at: string;
93 | };
94 | items: Array<{
95 | product: {
96 | name: string;
97 | };
98 | price: {
99 | unit_price: {
100 | amount: string;
101 | currency_code: string;
102 | };
103 | };
104 | }>;
105 | }>;
106 | };
107 | api_key: string;
108 | }
109 |
110 | export interface SystempromptPromptAPIRequest {
111 | metadata: Partial<Metadata>;
112 | instruction: {
113 | static: string;
114 | dynamic: string;
115 | state: string;
116 | };
117 | input: {
118 | type: string[];
119 | schema: JSONSchema7;
120 | name: string;
121 | description: string;
122 | };
123 | output: {
124 | type: string[];
125 | schema: JSONSchema7;
126 | name: string;
127 | description: string;
128 | };
129 | }
130 |
131 | export interface SystempromptPromptResponse {
132 | id: string;
133 | metadata: Metadata;
134 | instruction: {
135 | static: string;
136 | dynamic: string;
137 | state: string;
138 | };
139 | input: {
140 | name: string;
141 | description: string;
142 | type: string[];
143 | schema: JSONSchema7;
144 | };
145 | output: {
146 | name: string;
147 | description: string;
148 | type: string[];
149 | schema: JSONSchema7;
150 | };
151 | _link: string;
152 | }
153 |
154 | export interface Metadata {
155 | title: string;
156 | description: string | null;
157 | created: string;
158 | updated: string;
159 | version: number;
160 | status: string;
161 | author: string;
162 | log_message: string;
163 | tag: string[];
164 | }
165 |
```
--------------------------------------------------------------------------------
/src/constants/instructions.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const EMAIL_SEARCH_INSTRUCTIONS = `You are an expert at searching through email messages. Your task is to help users find relevant emails by constructing effective search queries.
2 |
3 | INPUT PARAMETERS:
4 | - userInstructions: Search requirements and criteria (required)
5 |
6 | YOUR ROLE:
7 | 1. Analyze the search requirements to identify:
8 | - Key search terms
9 | - Date ranges if specified
10 | - Sender/recipient filters
11 | - Other relevant criteria
12 |
13 | 2. Construct an effective search that considers:
14 | - Gmail search operators
15 | - Relevant labels/folders
16 | - Date formatting requirements
17 | - Priority and importance
18 |
19 | GUIDELINES:
20 | 1. Use precise search operators
21 | 2. Consider date ranges carefully
22 | 3. Include sender/recipient filters when relevant
23 | 4. Handle attachments appropriately
24 | 5. Balance specificity with recall`;
25 |
26 | export const EMAIL_DRAFT_INSTRUCTIONS = `You are an expert at composing email drafts. Your task is to create well-structured email drafts that effectively communicate the intended message.
27 |
28 | INPUT PARAMETERS:
29 | - userInstructions: Email content and requirements (required)
30 |
31 | YOUR ROLE:
32 | 1. Analyze the email requirements to identify:
33 | - Core message and purpose
34 | - Tone and formality level
35 | - Required components
36 | - Recipient context
37 |
38 | 2. Create an email draft that includes:
39 | - Clear subject line
40 | - Appropriate greeting
41 | - Well-structured content
42 | - Professional closing
43 | - Necessary CC/BCC
44 |
45 | GUIDELINES:
46 | 1. Maintain professional tone
47 | 2. Be concise but complete
48 | 3. Use appropriate formatting
49 | 4. Include all necessary recipients
50 | 5. Consider email etiquette`;
51 |
52 | export const EMAIL_SEND_INSTRUCTIONS = `You are an expert at composing and sending emails. Your task is to create and send well-structured emails or replies that effectively communicate the intended message.
53 |
54 | INPUT PARAMETERS:
55 | - userInstructions: Email requirements (required)
56 | - to: Recipient email address(es) (required)
57 | - replyTo: Optional message ID to reply to
58 |
59 | YOUR ROLE:
60 | 1. Analyze the email requirements to identify:
61 | - Whether this is a new email or a reply
62 | - Core message and purpose
63 | - Tone and formality level
64 | - Required components
65 | - Recipient context
66 |
67 | 2. Create an email that includes:
68 | - Clear subject line (for new emails)
69 | - Appropriate greeting
70 | - Well-structured content
71 | - Professional closing
72 |
73 | GUIDELINES:
74 | 1. Maintain professional tone
75 | 2. Be concise but complete
76 | 3. Use appropriate formatting
77 | 4. Include all necessary recipients
78 | 5. Consider email etiquette
79 | 6. Handle attachments appropriately
80 | 7. Use HTML formatting when beneficial
81 | 8. For replies:
82 | - Maintain email thread context
83 | - Quote relevant parts when needed
84 | - Keep subject consistent
85 |
86 | OUTPUT:
87 | Return a JSON object with:
88 | - to: Recipient email(s)
89 | - subject: Clear subject line (for new emails)
90 | - body: Well-formatted content
91 | - cc: CC recipients (if needed)
92 | - bcc: BCC recipients (if needed)
93 | - isHtml: Whether to use HTML formatting
94 | - replyTo: Message ID being replied to (if applicable)
95 | - attachments: Any required attachments`;
96 |
```
--------------------------------------------------------------------------------
/src/utils/__tests__/message-handlers.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Mock setup
2 | jest.mock("../../server", () => ({
3 | server: {
4 | notification: jest.fn(),
5 | },
6 | }));
7 |
8 | import {
9 | updateUserMessageWithContent,
10 | injectVariablesIntoText,
11 | injectVariables,
12 | } from "../message-handlers";
13 | import { XML_TAGS } from "../../constants/message-handler";
14 | import type { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
15 |
16 | describe("message-handlers", () => {
17 | describe("updateUserMessageWithContent", () => {
18 | it("should update user message with content", () => {
19 | const messages: PromptMessage[] = [
20 | {
21 | role: "user",
22 | content: {
23 | type: "text",
24 | text: `test message${XML_TAGS.REQUEST_PARAMS_CLOSE}`,
25 | },
26 | },
27 | ];
28 | const blocks = { test: "data" };
29 |
30 | updateUserMessageWithContent(messages, blocks);
31 |
32 | expect(messages[0].content.type).toBe("text");
33 | expect(messages[0].content.text).toContain(
34 | JSON.stringify(blocks, null, 2)
35 | );
36 | });
37 |
38 | it("should not modify messages if no user message exists", () => {
39 | const messages: PromptMessage[] = [
40 | { role: "assistant", content: { type: "text", text: "test" } },
41 | ];
42 | const originalMessages = [...messages];
43 |
44 | updateUserMessageWithContent(messages, {});
45 |
46 | expect(messages).toEqual(originalMessages);
47 | });
48 | });
49 |
50 | describe("injectVariablesIntoText", () => {
51 | it("should inject variables into text", () => {
52 | const text = "Hello {{name}}, your age is {{age}}";
53 | const variables = { name: "John", age: 30 };
54 |
55 | const result = injectVariablesIntoText(text, variables);
56 |
57 | expect(result).toBe("Hello John, your age is 30");
58 | });
59 |
60 | it("should handle missing variables", () => {
61 | const text = "Hello {{name}}";
62 | const variables = { name: "John" };
63 |
64 | const result = injectVariablesIntoText(text, variables);
65 |
66 | expect(result).toBe("Hello John");
67 | });
68 |
69 | it("should throw error for missing required variables", () => {
70 | const text = "Hello {{name}}, your age is {{age}}";
71 | const variables = { name: "John" };
72 |
73 | expect(() => injectVariablesIntoText(text, variables)).toThrow(
74 | "Missing required variables: age"
75 | );
76 | });
77 | });
78 |
79 | describe("injectVariables", () => {
80 | it("should inject variables into text message", () => {
81 | const message: PromptMessage = {
82 | role: "user",
83 | content: { type: "text", text: "Hello {{name}}" },
84 | };
85 | const variables = { name: "John" };
86 |
87 | const result = injectVariables(message, variables);
88 |
89 | expect(result.content.type).toBe("text");
90 | expect(result.content.text).toBe("Hello John");
91 | });
92 |
93 | it("should return original message for non-text content", () => {
94 | const message: PromptMessage = {
95 | role: "user",
96 | content: {
97 | type: "image",
98 | data: "base64data",
99 | mimeType: "image/jpeg",
100 | },
101 | };
102 | const variables = { name: "John" };
103 |
104 | const result = injectVariables(message, variables);
105 | expect(result).toEqual(message);
106 | });
107 | });
108 | });
109 |
```
--------------------------------------------------------------------------------
/src/utils/__tests__/tool-validation.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { validateToolRequest, getToolSchema } from "../tool-validation";
2 | import { TOOLS } from "../../constants/tools";
3 | import type { CallToolRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
4 | import type { JSONSchema7 } from "json-schema";
5 |
6 | // Mock the tools constant
7 | jest.mock("../../constants/tools", () => ({
8 | TOOLS: [
9 | {
10 | name: "test_tool",
11 | description: "A test tool",
12 | inputSchema: {
13 | type: "object",
14 | properties: {
15 | test: { type: "string" },
16 | },
17 | required: ["test"],
18 | },
19 | },
20 | {
21 | name: "simple_tool",
22 | description: "A simple tool without schema",
23 | },
24 | ],
25 | }));
26 |
27 | describe("tool-validation", () => {
28 | describe("validateToolRequest", () => {
29 | it("should validate a tool request with valid parameters", () => {
30 | const request: CallToolRequest = {
31 | method: "tools/call",
32 | params: {
33 | name: "test_tool",
34 | arguments: {
35 | test: "value",
36 | },
37 | },
38 | };
39 |
40 | const result = validateToolRequest(request);
41 | expect(result).toBeDefined();
42 | expect(result.name).toBe("test_tool");
43 | });
44 |
45 | it("should validate a tool request without schema", () => {
46 | const request: CallToolRequest = {
47 | method: "tools/call",
48 | params: {
49 | name: "simple_tool",
50 | arguments: {},
51 | },
52 | };
53 |
54 | const result = validateToolRequest(request);
55 | expect(result).toBeDefined();
56 | expect(result.name).toBe("simple_tool");
57 | });
58 |
59 | it("should throw error for missing tool name", () => {
60 | const request = {
61 | method: "tools/call",
62 | params: {
63 | arguments: {},
64 | },
65 | } as CallToolRequest;
66 |
67 | expect(() => validateToolRequest(request)).toThrow(
68 | "Invalid tool request: missing tool name"
69 | );
70 | });
71 |
72 | it("should throw error for unknown tool", () => {
73 | const request: CallToolRequest = {
74 | method: "tools/call",
75 | params: {
76 | name: "unknown_tool",
77 | arguments: {},
78 | },
79 | };
80 |
81 | expect(() => validateToolRequest(request)).toThrow(
82 | "Unknown tool: unknown_tool"
83 | );
84 | });
85 |
86 | it("should throw error for invalid arguments", () => {
87 | const request: CallToolRequest = {
88 | method: "tools/call",
89 | params: {
90 | name: "test_tool",
91 | arguments: {
92 | wrong: "value", // Missing required 'test' field
93 | },
94 | },
95 | };
96 |
97 | expect(() => validateToolRequest(request)).toThrow();
98 | });
99 | });
100 |
101 | describe("getToolSchema", () => {
102 | it("should return schema for tool with schema", () => {
103 | const schema = getToolSchema("test_tool");
104 | expect(schema).toBeDefined();
105 | expect(schema?.type).toBe("object");
106 | expect(schema?.properties?.test).toBeDefined();
107 | });
108 |
109 | it("should return undefined for tool without schema", () => {
110 | const schema = getToolSchema("simple_tool");
111 | expect(schema).toBeUndefined();
112 | });
113 |
114 | it("should return undefined for unknown tool", () => {
115 | const schema = getToolSchema("unknown_tool");
116 | expect(schema).toBeUndefined();
117 | });
118 | });
119 | });
120 |
```
--------------------------------------------------------------------------------
/src/__mocks__/systemprompt-service.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { SystempromptBlockResponse } from "../types/systemprompt.js";
2 |
3 | class MockSystemPromptService {
4 | private static instance: MockSystemPromptService;
5 | private initialized = false;
6 | private apiKey: string | null = null;
7 |
8 | private constructor() {}
9 |
10 | public static getInstance(): MockSystemPromptService {
11 | if (!MockSystemPromptService.instance) {
12 | MockSystemPromptService.instance = new MockSystemPromptService();
13 | }
14 | return MockSystemPromptService.instance;
15 | }
16 |
17 | public initialize(apiKey: string): void {
18 | this.apiKey = apiKey;
19 | this.initialized = true;
20 | }
21 |
22 | private checkInitialized(): void {
23 | if (!this.initialized || !this.apiKey) {
24 | throw new Error(
25 | "SystemPromptService must be initialized with an API key first"
26 | );
27 | }
28 | }
29 |
30 | public async listBlocks(): Promise<SystempromptBlockResponse[]> {
31 | this.checkInitialized();
32 | return [
33 | {
34 | id: "default",
35 | content: JSON.stringify({
36 | name: "Systemprompt Agent",
37 | description:
38 | "An expert agent for managing and organizing content in workspaces",
39 | instruction: "You are a specialized agent",
40 | voice: "Kore",
41 | config: {
42 | model: "models/gemini-2.0-flash-exp",
43 | generationConfig: {
44 | responseModalities: "audio",
45 | speechConfig: {
46 | voiceConfig: {
47 | prebuiltVoiceConfig: {
48 | voiceName: "Kore",
49 | },
50 | },
51 | },
52 | },
53 | },
54 | }),
55 | metadata: {
56 | title: "Systemprompt Agent",
57 | description:
58 | "An expert agent for managing and organizing content in workspaces",
59 | created: new Date().toISOString(),
60 | updated: new Date().toISOString(),
61 | version: 1,
62 | status: "active",
63 | author: "system",
64 | log_message: "Initial version",
65 | tag: ["agent"],
66 | },
67 | prefix: "",
68 | },
69 | ];
70 | }
71 |
72 | public async getBlock(id: string): Promise<SystempromptBlockResponse> {
73 | this.checkInitialized();
74 | if (id === "default") {
75 | return {
76 | id: "default",
77 | content: JSON.stringify({
78 | name: "Systemprompt Agent",
79 | description:
80 | "An expert agent for managing and organizing content in workspaces",
81 | instruction: "You are a specialized agent",
82 | voice: "Kore",
83 | config: {
84 | model: "models/gemini-2.0-flash-exp",
85 | generationConfig: {
86 | responseModalities: "audio",
87 | speechConfig: {
88 | voiceConfig: {
89 | prebuiltVoiceConfig: {
90 | voiceName: "Kore",
91 | },
92 | },
93 | },
94 | },
95 | },
96 | }),
97 | metadata: {
98 | title: "Systemprompt Agent",
99 | description:
100 | "An expert agent for managing and organizing content in workspaces",
101 | created: new Date().toISOString(),
102 | updated: new Date().toISOString(),
103 | version: 1,
104 | status: "active",
105 | author: "system",
106 | log_message: "Initial version",
107 | tag: ["agent"],
108 | },
109 | prefix: "",
110 | };
111 | }
112 | throw new Error("Resource not found");
113 | }
114 | }
115 |
116 | export default MockSystemPromptService;
117 |
```
--------------------------------------------------------------------------------
/src/utils/mcp-mappers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | GetPromptResult,
3 | ListResourcesResult,
4 | ListPromptsResult,
5 | ReadResourceResult,
6 | } from "@modelcontextprotocol/sdk/types.js";
7 | import type {
8 | SystempromptPromptResponse,
9 | SystempromptBlockResponse,
10 | SystempromptAgentResponse,
11 | } from "../types/systemprompt.js";
12 |
13 | /**
14 | * Maps input schema properties to MCP argument format.
15 | * Shared between single prompt and list prompt mappings.
16 | */
17 | function mapPromptArguments(prompt: SystempromptPromptResponse) {
18 | return Object.entries(prompt.input.schema.properties || {})
19 | .map(([name, schema]) => {
20 | if (typeof schema === "boolean") return null;
21 | if (typeof schema !== "object" || schema === null) return null;
22 | return {
23 | name,
24 | description:
25 | "description" in schema ? String(schema.description || "") : "",
26 | required: prompt.input.schema.required?.includes(name) || false,
27 | };
28 | })
29 | .filter((arg): arg is NonNullable<typeof arg> => arg !== null);
30 | }
31 |
32 | /**
33 | * Maps a single prompt to the MCP GetPromptResult format.
34 | * Used when retrieving a single prompt's details.
35 | */
36 | export function mapPromptToGetPromptResult(
37 | prompt: SystempromptPromptResponse
38 | ): GetPromptResult {
39 | return {
40 | name: prompt.metadata.title,
41 | description: prompt.metadata.description || undefined,
42 | messages: [
43 | {
44 | role: "assistant",
45 | content: {
46 | type: "text",
47 | text: prompt.instruction.static,
48 | },
49 | },
50 | ],
51 | arguments: mapPromptArguments(prompt),
52 | tools: [],
53 | _meta: { prompt },
54 | };
55 | }
56 |
57 | /**
58 | * Maps an array of prompts to the MCP ListPromptsResult format.
59 | * Used when listing multiple prompts.
60 | */
61 | export function mapPromptsToListPromptsResult(
62 | prompts: SystempromptPromptResponse[]
63 | ): ListPromptsResult {
64 | return {
65 | _meta: { prompts },
66 | prompts: prompts.map((prompt) => ({
67 | name: prompt.metadata.title,
68 | description: prompt.metadata.description || undefined,
69 | arguments: mapPromptArguments(prompt),
70 | })),
71 | };
72 | }
73 |
74 | /**
75 | * Maps a single block to the MCP ReadResourceResult format.
76 | * Used when retrieving a single block's details.
77 | */
78 | export function mapBlockToReadResourceResult(
79 | block: SystempromptBlockResponse
80 | ): ReadResourceResult {
81 | return {
82 | contents: [
83 | {
84 | uri: `resource:///block/${block.id}`,
85 | mimeType: "text/plain",
86 | text: block.content,
87 | },
88 | ],
89 | _meta: {},
90 | };
91 | }
92 |
93 | export function mapAgentToReadResourceResult(
94 | agent: SystempromptAgentResponse
95 | ): ReadResourceResult {
96 | return {
97 | contents: [
98 | {
99 | uri: `resource:///agent/${agent.id}`,
100 | mimeType: "text/plain",
101 | text: agent.content,
102 | },
103 | ],
104 | _meta: {
105 | agent: true
106 | },
107 | };
108 | }
109 |
110 |
111 | /**
112 | * Maps an array of blocks to the MCP ListResourcesResult format.
113 | * Used when listing multiple blocks.
114 | */
115 | export function mapBlocksToListResourcesResult(
116 | blocks: SystempromptBlockResponse[]
117 | ): ListResourcesResult {
118 | return {
119 | _meta: {},
120 | resources: blocks.map((block) => ({
121 | uri: `resource:///block/${block.id}`,
122 | name: block.metadata.title,
123 | description: block.metadata.description || undefined,
124 | mimeType: "text/plain",
125 | })),
126 | };
127 | }
128 |
129 | export function mapAgentsToListResourcesResult(
130 | agents: SystempromptAgentResponse[]
131 | ): ListResourcesResult {
132 | return {
133 | _meta: {},
134 | resources: agents.map((agent) => ({
135 | uri: `resource:///agent/${agent.id}`,
136 | name: agent.metadata.title,
137 | description: agent.metadata.description || undefined,
138 | mimeType: "text/plain",
139 | _meta: {
140 | agent: true
141 | }
142 | })),
143 | };
144 | }
145 |
146 |
```
--------------------------------------------------------------------------------
/src/types/sampling-schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { JSONSchema7 } from "json-schema";
2 |
3 | export const EMAIL_SEND_RESPONSE_SCHEMA: JSONSchema7 = {
4 | type: "object",
5 | properties: {
6 | to: {
7 | type: "string",
8 | description: "Recipient email address(es). Multiple addresses can be comma-separated.",
9 | },
10 | subject: {
11 | type: "string",
12 | description: "Email subject line.",
13 | },
14 | body: {
15 | type: "string",
16 | description: "Email body content. Must be valid HTML.",
17 | },
18 | cc: {
19 | type: "string",
20 | description: "CC recipient email address(es). Multiple addresses can be comma-separated.",
21 | },
22 | bcc: {
23 | type: "string",
24 | description: "BCC recipient email address(es). Multiple addresses can be comma-separated.",
25 | },
26 | isHtml: {
27 | type: "boolean",
28 | description: "Whether the body content is HTML. Defaults to false for plain text.",
29 | },
30 | replyTo: {
31 | type: "string",
32 | description: "Reply-to email address.",
33 | },
34 | attachments: {
35 | type: "array",
36 | description: "List of attachments to include in the email.",
37 | items: {
38 | type: "object",
39 | properties: {
40 | filename: {
41 | type: "string",
42 | description: "Name of the attachment file.",
43 | },
44 | content: {
45 | type: "string",
46 | description: "Content of the attachment (base64 encoded for binary files).",
47 | },
48 | contentType: {
49 | type: "string",
50 | description: "MIME type of the attachment.",
51 | },
52 | },
53 | required: ["filename", "content"],
54 | },
55 | },
56 | },
57 | required: ["to", "subject", "body"],
58 | };
59 |
60 | export const EMAIL_REPLY_RESPONSE_SCHEMA: JSONSchema7 = {
61 | type: "object",
62 | properties: {
63 | replyTo: {
64 | type: "string",
65 | description: "Message ID to reply to.",
66 | },
67 | body: {
68 | type: "string",
69 | description: "Email body content. Must be valid HTML if isHtml is true.",
70 | },
71 | isHtml: {
72 | type: "boolean",
73 | description: "Whether the body content is HTML. Defaults to false for plain text.",
74 | },
75 | },
76 | required: ["replyTo", "body"],
77 | };
78 |
79 | export const DRAFT_EMAIL_RESPONSE_SCHEMA: JSONSchema7 = {
80 | type: "object",
81 | properties: {
82 | to: {
83 | type: "string",
84 | description: "Recipient email address(es). Multiple addresses can be comma-separated.",
85 | },
86 | subject: {
87 | type: "string",
88 | description: "Email subject line.",
89 | },
90 | body: {
91 | type: "string",
92 | description: "Email body content. Must be valid HTML.",
93 | },
94 | cc: {
95 | type: "string",
96 | description: "CC recipient email address(es). Multiple addresses can be comma-separated.",
97 | },
98 | bcc: {
99 | type: "string",
100 | description: "BCC recipient email address(es). Multiple addresses can be comma-separated.",
101 | },
102 | isHtml: {
103 | type: "boolean",
104 | description: "Whether the body content is HTML. Defaults to false for plain text.",
105 | },
106 | replyTo: {
107 | type: "string",
108 | description: "Message ID to reply to for draft replies.",
109 | },
110 | id: {
111 | type: "string",
112 | description: "Draft ID for updating existing drafts.",
113 | },
114 | attachments: {
115 | type: "array",
116 | description: "List of attachments to include in the email.",
117 | items: {
118 | type: "object",
119 | properties: {
120 | filename: {
121 | type: "string",
122 | description: "Name of the attachment file.",
123 | },
124 | content: {
125 | type: "string",
126 | description: "Content of the attachment (base64 encoded for binary files).",
127 | },
128 | contentType: {
129 | type: "string",
130 | description: "MIME type of the attachment.",
131 | },
132 | },
133 | required: ["filename", "content"],
134 | },
135 | },
136 | },
137 | required: ["to", "subject", "body"],
138 | };
139 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | ## [1.0.23] - 2025-01-23
4 |
5 | ### Fixed
6 |
7 | - Fixed validation errors with ajv
8 |
9 | All notable changes to this project will be documented in this file.
10 |
11 |
12 |
13 | ## [1.0.22] - 2025-01-23
14 |
15 | ### Fixed
16 |
17 | - Fixed validation errors with ajv
18 |
19 | All notable changes to this project will be documented in this file.
20 |
21 |
22 | ## [1.0.21] - 2025-01-23
23 |
24 | ### Fixed
25 |
26 | - Fixed validation errors with ajv
27 |
28 | All notable changes to this project will be documented in this file.
29 |
30 | ## [1.0.18] - 2025-01-23
31 |
32 | ### Changed
33 |
34 | - Updated commit state and version tracking
35 | - Synchronized version information across package files
36 |
37 | ## [1.0.17] - 2025-01-13
38 |
39 | ### Fixed
40 |
41 | - Enhanced error handling in SystemPromptService to properly handle 204 No Content responses
42 | - Improved HTTP status code handling with specific error messages for 400, 403, 404, and 409 responses
43 | - Fixed test expectations to match the new error messages
44 | - Improved test consistency by using a consistent base URL configuration
45 |
46 | ## [1.0.16] - 2025-01-13
47 |
48 | ### Fixed
49 |
50 | - Updated tests to match the correct resource URI format (`resource:///block/{id}`)
51 | - Fixed test expectations in resource handlers and mappers
52 |
53 | ## [1.0.15] - 2025-01-13
54 |
55 | ### Fixed
56 |
57 | - Fixed resource URI format in list resources response to consistently use `resource:///block/{id}` format
58 | - Improved handling of null descriptions in resource list response
59 |
60 | ## [1.0.14] - 2025-01-13
61 |
62 | ### Fixed
63 |
64 | - Modified resource handler to accept both plain UUIDs and full resource URIs
65 | - Improved compatibility with MCP resource protocol
66 |
67 | ## [1.0.13] - 2025-01-13
68 |
69 | ### Fixed
70 |
71 | - Fixed resource response format in `systemprompt_fetch_resource` tool to match MCP protocol requirements
72 | - Updated tests to match the new resource response format
73 |
74 | ## [1.0.12] - 2025-01-13
75 |
76 | ### Changed
77 |
78 | - Enhanced README with badges, links, and improved documentation structure
79 | - Improved configuration documentation with detailed environment variables and feature flags
80 | - Updated service documentation with comprehensive API integration details
81 | - Enhanced handlers documentation with detailed implementation examples
82 |
83 | ## [1.0.11] - 2025-01-13
84 |
85 | ### Changed
86 |
87 | - Added comprehensive test coverage for notification handling
88 | - Enhanced test organization for tool handlers and prompt handlers
89 | - Improved test structure with better mocking patterns for resource handlers
90 |
91 | ## [1.0.10] - 2025-01-13
92 |
93 | ### Changed
94 |
95 | - Enhanced test coverage and organization in prompt and tool handlers
96 | - Updated tsconfig.json to properly exclude test and mock directories
97 | - Improved test structure with better mocking patterns
98 |
99 | ## [1.0.9] - 2025-01-10
100 |
101 | ### Added
102 |
103 | - Added new `systemprompt_fetch_resource` tool for retrieving resource content
104 | - Added metadata to prompt and resource responses for better debugging
105 |
106 | ### Changed
107 |
108 | - Refactored prompt argument mapping for improved consistency
109 | - Enhanced prompt result mapping with more detailed metadata
110 |
111 | ## [1.0.6] - 2025-01-09
112 |
113 | ### Changed
114 |
115 | - Enhanced README with comprehensive agent management capabilities
116 | - Added API key requirement notice and link to console
117 | - Updated tools section with accurate tool names and descriptions
118 | - Improved documentation structure and readability
119 | - Removed redundant testing documentation
120 |
121 | ## [1.0.5] - 2025-01-09
122 |
123 | ### Changed
124 |
125 | - Made description field nullable in resource types for better type safety
126 | - Improved error handling for null descriptions in resource handlers
127 | - Enhanced test coverage for empty API key initialization and null descriptions
128 | - Refactored test mocks for better type safety
129 |
130 | ## [1.0.4] - 2025-01-09
131 |
132 | ### Added
133 |
134 | - Added CLI support through npx with proper binary configuration
135 | - Added shebang line for direct script execution
136 |
137 | ### Changed
138 |
139 | - Improved server process handling with proper stdin management
140 | - Removed unnecessary console logging for better stdio transport compatibility
141 |
142 | ## [1.0.3] - 2025-01-09
143 |
144 | ### Changed
145 |
146 | - Refactored SystemPromptService to use singleton pattern consistently
147 | - Improved test implementations with better mocking and error handling
148 | - Enhanced type definitions and schema validation in handlers
149 |
150 | ## [1.0.2] - 2025-01-09
151 |
152 | ### Fixed
153 |
154 | - Rebuilt package to ensure latest changes are included in the published version
155 |
156 | ## [1.0.1] - 2025-01-09
157 |
158 | ### Changed
159 |
160 | - Updated package metadata with proper repository, homepage, and bug tracker URLs
161 | - Added keywords for better npm discoverability
162 | - Added engine requirement for Node.js >= 18.0.0
163 | - Added MIT license specification
164 |
165 | ## [1.0.0] - 2025-01-09
166 |
167 | ### Breaking Changes
168 |
169 | - Renamed `content` property to `contentUrl` in `SystemPromptResource` interface and all implementations to better reflect its purpose
170 |
```
--------------------------------------------------------------------------------
/src/__tests__/test-utils.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest, describe, it, expect } from "@jest/globals";
2 | import {
3 | createMockResponse,
4 | TestFixtures,
5 | flushPromises,
6 | isError,
7 | createPartialMock,
8 | } from "./test-utils";
9 |
10 | describe("Test Utilities", () => {
11 | describe("createMockResponse", () => {
12 | it("should create a successful response with defaults", async () => {
13 | const data = { test: "data" };
14 | const response = createMockResponse(data);
15 |
16 | expect(response.ok).toBe(true);
17 | expect(response.status).toBe(200);
18 | expect(response.statusText).toBe("OK");
19 | expect(response.headers).toBeInstanceOf(Headers);
20 |
21 | // Test response methods
22 | expect(await response.json()).toEqual(data);
23 | expect(await response.text()).toBe(JSON.stringify(data));
24 | expect(await response.blob()).toBeInstanceOf(Blob);
25 | });
26 |
27 | it("should create a response with custom options", async () => {
28 | const data = "test-data";
29 | const options = {
30 | ok: false,
31 | status: 400,
32 | statusText: "Bad Request",
33 | headers: { "Content-Type": "text/plain" },
34 | };
35 | const response = createMockResponse(data, options);
36 |
37 | expect(response.ok).toBe(false);
38 | expect(response.status).toBe(400);
39 | expect(response.statusText).toBe("Bad Request");
40 | expect(response.headers.get("Content-Type")).toBe("text/plain");
41 |
42 | // Test string data handling
43 | expect(await response.text()).toBe(data);
44 | });
45 | });
46 |
47 | describe("TestFixtures", () => {
48 | it("should create a note with default values", () => {
49 | const note = TestFixtures.createNote();
50 |
51 | expect(note).toHaveProperty("id", "test-note-1");
52 | expect(note).toHaveProperty("title", "Test Note");
53 | expect(note).toHaveProperty("content", "Test content");
54 | expect(note.created).toMatch(
55 | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
56 | );
57 | });
58 |
59 | it("should create a note with custom overrides", () => {
60 | const overrides = {
61 | id: "custom-id",
62 | title: "Custom Title",
63 | extraField: "extra",
64 | };
65 | const note = TestFixtures.createNote(overrides);
66 |
67 | expect(note).toMatchObject(overrides);
68 | expect(note).toHaveProperty("content", "Test content");
69 | expect(note.created).toBeDefined();
70 | });
71 |
72 | it("should create a list of notes with specified count", () => {
73 | const count = 3;
74 | const notes = TestFixtures.createNoteList(count);
75 |
76 | expect(notes).toHaveLength(count);
77 | notes.forEach((note, index) => {
78 | expect(note).toHaveProperty("id", `test-note-${index + 1}`);
79 | expect(note).toHaveProperty("title", "Test Note");
80 | expect(note).toHaveProperty("content", "Test content");
81 | expect(note.created).toBeDefined();
82 | });
83 | });
84 | });
85 |
86 | describe("flushPromises", () => {
87 | it("should wait for promises to resolve", async () => {
88 | let resolved = false;
89 | Promise.resolve().then(() => {
90 | resolved = true;
91 | });
92 |
93 | expect(resolved).toBe(false);
94 | await flushPromises();
95 | expect(resolved).toBe(true);
96 | });
97 |
98 | it("should handle multiple promises", async () => {
99 | const results: number[] = [];
100 | Promise.resolve().then(() => results.push(1));
101 | Promise.resolve().then(() => results.push(2));
102 | Promise.resolve().then(() => results.push(3));
103 |
104 | expect(results).toHaveLength(0);
105 | await flushPromises();
106 | expect(results).toEqual([1, 2, 3]);
107 | });
108 | });
109 |
110 | describe("isError", () => {
111 | it("should identify Error objects", () => {
112 | expect(isError(new Error("test"))).toBe(true);
113 | expect(isError(new TypeError("test"))).toBe(true);
114 | });
115 |
116 | it("should reject non-Error objects", () => {
117 | expect(isError({})).toBe(false);
118 | expect(isError("error")).toBe(false);
119 | expect(isError(null)).toBe(false);
120 | expect(isError(undefined)).toBe(false);
121 | expect(isError(42)).toBe(false);
122 | });
123 | });
124 |
125 | describe("createPartialMock", () => {
126 | interface TestService {
127 | method1(): string;
128 | method2(arg: number): Promise<number>;
129 | }
130 |
131 | it("should create a typed mock object", () => {
132 | const mockMethod = jest.fn<() => string>().mockReturnValue("test");
133 | const mock = createPartialMock<TestService>({
134 | method1: mockMethod,
135 | });
136 |
137 | expect(mock.method1()).toBe("test");
138 | expect(mock.method1).toHaveBeenCalled();
139 | });
140 |
141 | it("should handle empty overrides", () => {
142 | const mock = createPartialMock<TestService>();
143 | expect(mock).toEqual({});
144 | });
145 |
146 | it("should preserve mock functionality", async () => {
147 | const mockMethod = jest
148 | .fn<(arg: number) => Promise<number>>()
149 | .mockResolvedValue(42);
150 | const mock = createPartialMock<TestService>({
151 | method2: mockMethod,
152 | });
153 |
154 | const result = await mock.method2(1);
155 | expect(result).toBe(42);
156 | expect(mock.method2).toHaveBeenCalledWith(1);
157 | });
158 | });
159 | });
160 |
```
--------------------------------------------------------------------------------
/src/constants/sampling-prompts.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SamplingPrompt } from "../types/sampling.js";
2 | import { EMAIL_SEND_INSTRUCTIONS } from "./instructions.js";
3 | import {
4 | EMAIL_SEND_RESPONSE_SCHEMA,
5 | EMAIL_REPLY_RESPONSE_SCHEMA,
6 | DRAFT_EMAIL_RESPONSE_SCHEMA,
7 | } from "../types/sampling-schemas.js";
8 |
9 | const promptArgs = [
10 | {
11 | name: "userInstructions",
12 | description: "Instructions for the email operation",
13 | required: true,
14 | },
15 | ];
16 |
17 | // Email Send Prompt
18 | export const SEND_EMAIL_PROMPT: SamplingPrompt = {
19 | name: "gmail_send_email",
20 | description: "Sends an email or reply based on user instructions",
21 | arguments: [
22 | ...promptArgs,
23 | {
24 | name: "to",
25 | description: "Recipient email address(es)",
26 | required: true,
27 | },
28 | {
29 | name: "messageId",
30 | description: "Optional message ID to reply to",
31 | required: false,
32 | },
33 | ],
34 | messages: [
35 | {
36 | role: "assistant",
37 | content: {
38 | type: "text",
39 | text: EMAIL_SEND_INSTRUCTIONS,
40 | },
41 | },
42 | {
43 | role: "user",
44 | content: {
45 | type: "text",
46 | text: `<input>
47 | <userInstructions>{{userInstructions}}</userInstructions>
48 | <to>{{to}}</to>
49 | {{#messageId}}<replyTo>{{messageId}}</replyTo>{{/messageId}}
50 | </input>`,
51 | },
52 | },
53 | ],
54 | _meta: {
55 | callback: "send_email",
56 | responseSchema: EMAIL_SEND_RESPONSE_SCHEMA,
57 | },
58 | };
59 |
60 | // Email Send Prompt
61 | export const REPLY_EMAIL_PROMPT: SamplingPrompt = {
62 | name: "gmail_reply_email",
63 | description: "Sends an email or reply based on user instructions",
64 | arguments: [
65 | ...promptArgs,
66 | {
67 | name: "to",
68 | description: "Recipient email address(es)",
69 | required: true,
70 | },
71 | {
72 | name: "messageId",
73 | description: "Optional message ID to reply to",
74 | required: false,
75 | },
76 | ],
77 | messages: [
78 | {
79 | role: "assistant",
80 | content: {
81 | type: "text",
82 | text: EMAIL_SEND_INSTRUCTIONS,
83 | },
84 | },
85 | {
86 | role: "assistant",
87 | content: {
88 | type: "text",
89 | text: `<History>
90 | <threadContent>{{threadContent}}</threadContent>
91 | </History>`,
92 | },
93 | },
94 | {
95 | role: "user",
96 | content: {
97 | type: "text",
98 | text: `<input>
99 | <userInstructions>{{userInstructions}}</userInstructions>
100 | <to>{{to}}</to>
101 | <replyTo>{{messageId}}</replyTo>
102 | </input>`,
103 | },
104 | },
105 | ],
106 | _meta: {
107 | callback: "reply_email",
108 | responseSchema: EMAIL_REPLY_RESPONSE_SCHEMA,
109 | },
110 | };
111 |
112 | // Email Send Prompt
113 | export const REPLY_DRAFT_PROMPT: SamplingPrompt = {
114 | name: "gmail_reply_draft",
115 | description: "Replies to a draft email based on user instructions",
116 | arguments: [
117 | ...promptArgs,
118 | {
119 | name: "to",
120 | description: "Recipient email address(es)",
121 | required: true,
122 | },
123 | {
124 | name: "messageId",
125 | description: "Optional message ID to reply to",
126 | required: false,
127 | },
128 | ],
129 | messages: [
130 | {
131 | role: "assistant",
132 | content: {
133 | type: "text",
134 | text: EMAIL_SEND_INSTRUCTIONS,
135 | },
136 | },
137 | {
138 | role: "user",
139 | content: {
140 | type: "text",
141 | text: `<input>
142 | <userInstructions>{{userInstructions}}</userInstructions>
143 | <to>{{to}}</to>
144 | {{#messageId}}<replyTo>{{messageId}}</replyTo>{{/messageId}}
145 | </input>`,
146 | },
147 | },
148 | ],
149 | _meta: {
150 | callback: "reply_draft",
151 | responseSchema: DRAFT_EMAIL_RESPONSE_SCHEMA,
152 | },
153 | };
154 |
155 | // Email Send Prompt
156 | export const EDIT_DRAFT_PROMPT: SamplingPrompt = {
157 | name: "gmail_edit_draft",
158 | description: "Edits a draft email based on user instructions",
159 | arguments: [
160 | ...promptArgs,
161 | {
162 | name: "to",
163 | description: "Recipient email address(es)",
164 | required: true,
165 | },
166 | {
167 | name: "messageId",
168 | description: "Optional message ID to reply to",
169 | required: false,
170 | },
171 | ],
172 | messages: [
173 | {
174 | role: "assistant",
175 | content: {
176 | type: "text",
177 | text: EMAIL_SEND_INSTRUCTIONS,
178 | },
179 | },
180 | {
181 | role: "user",
182 | content: {
183 | type: "text",
184 | text: `<input>
185 | <userInstructions>{{userInstructions}}</userInstructions>
186 | <to>{{to}}</to>
187 | {{#messageId}}<replyTo>{{messageId}}</replyTo>{{/messageId}}
188 | </input>`,
189 | },
190 | },
191 | ],
192 | _meta: {
193 | callback: "edit_draft",
194 | responseSchema: DRAFT_EMAIL_RESPONSE_SCHEMA,
195 | },
196 | };
197 |
198 | // Create Draft Prompt
199 | export const CREATE_DRAFT_PROMPT: SamplingPrompt = {
200 | name: "gmail_create_draft",
201 | description: "Creates a draft email based on user instructions",
202 | arguments: [
203 | ...promptArgs,
204 | {
205 | name: "to",
206 | description: "Recipient email address(es)",
207 | required: true,
208 | },
209 | ],
210 | messages: [
211 | {
212 | role: "assistant",
213 | content: {
214 | type: "text",
215 | text: EMAIL_SEND_INSTRUCTIONS,
216 | },
217 | },
218 | {
219 | role: "user",
220 | content: {
221 | type: "text",
222 | text: `<input>
223 | <userInstructions>{{userInstructions}}</userInstructions>
224 | <to>{{to}}</to>
225 | </input>`,
226 | },
227 | },
228 | ],
229 | _meta: {
230 | callback: "create_draft",
231 | responseSchema: DRAFT_EMAIL_RESPONSE_SCHEMA,
232 | },
233 | };
234 |
235 | // Export all prompts
236 | export const PROMPTS = [
237 | SEND_EMAIL_PROMPT,
238 | REPLY_EMAIL_PROMPT,
239 | REPLY_DRAFT_PROMPT,
240 | EDIT_DRAFT_PROMPT,
241 | CREATE_DRAFT_PROMPT,
242 | ];
243 |
```
--------------------------------------------------------------------------------
/src/services/__tests__/google-auth-service.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import { google } from "googleapis";
3 | import { OAuth2Client } from "google-auth-library";
4 | import { GoogleAuthService } from "../google-auth-service";
5 | import * as fs from "fs";
6 |
7 | jest.mock("googleapis");
8 | jest.mock("google-auth-library");
9 | jest.mock("fs");
10 |
11 | describe("GoogleAuthService", () => {
12 | let mockOAuth2Client: jest.Mocked<OAuth2Client>;
13 |
14 | beforeEach(() => {
15 | jest.clearAllMocks();
16 |
17 | mockOAuth2Client = {
18 | setCredentials: jest.fn(),
19 | } as unknown as jest.Mocked<OAuth2Client>;
20 |
21 | const MockOAuth2Client = jest.fn(() => mockOAuth2Client);
22 | (google.auth.OAuth2 as unknown as jest.Mock) = MockOAuth2Client;
23 | (fs.existsSync as jest.Mock).mockReturnValue(false);
24 | (fs.readFileSync as jest.Mock).mockReturnValue(
25 | JSON.stringify({
26 | installed: {
27 | client_id: "test-client-id",
28 | client_secret: "test-client-secret",
29 | redirect_uris: ["http://localhost"],
30 | },
31 | })
32 | );
33 | });
34 |
35 | describe("getInstance", () => {
36 | it("should create a singleton instance", () => {
37 | const instance1 = GoogleAuthService.getInstance();
38 | const instance2 = GoogleAuthService.getInstance();
39 | expect(instance1).toBe(instance2);
40 | });
41 | });
42 |
43 | describe("initialize", () => {
44 | it("should initialize OAuth2Client with credentials", async () => {
45 | const mockCredentials = {
46 | installed: {
47 | client_id: "test-client-id",
48 | client_secret: "test-client-secret",
49 | redirect_uris: ["http://localhost"],
50 | },
51 | };
52 |
53 | (fs.readFileSync as jest.Mock).mockReturnValueOnce(
54 | JSON.stringify(mockCredentials)
55 | );
56 |
57 | const service = GoogleAuthService.getInstance();
58 | await service.initialize();
59 |
60 | expect(google.auth.OAuth2).toHaveBeenCalledWith(
61 | "test-client-id",
62 | "test-client-secret",
63 | "http://localhost"
64 | );
65 | });
66 |
67 | it("should load existing token if available", async () => {
68 | const mockCredentials = {
69 | installed: {
70 | client_id: "test-client-id",
71 | client_secret: "test-client-secret",
72 | redirect_uris: ["http://localhost"],
73 | },
74 | };
75 | const mockToken = { access_token: "test-token" };
76 |
77 | (fs.readFileSync as jest.Mock)
78 | .mockReturnValueOnce(JSON.stringify(mockCredentials))
79 | .mockReturnValueOnce(JSON.stringify(mockToken));
80 | (fs.existsSync as jest.Mock).mockReturnValue(true);
81 |
82 | const service = GoogleAuthService.getInstance();
83 | await service.initialize();
84 |
85 | expect(mockOAuth2Client.setCredentials).toHaveBeenCalledWith(mockToken);
86 | });
87 |
88 | it("should handle missing credentials file", async () => {
89 | (fs.readFileSync as jest.Mock).mockImplementation(() => {
90 | throw new Error("File not found");
91 | });
92 |
93 | const service = GoogleAuthService.getInstance();
94 | await expect(service.initialize()).rejects.toThrow("File not found");
95 | });
96 | });
97 |
98 | describe("authenticate", () => {
99 | it("should skip authentication if token exists", async () => {
100 | const mockCredentials = {
101 | installed: {
102 | client_id: "test-client-id",
103 | client_secret: "test-client-secret",
104 | redirect_uris: ["http://localhost"],
105 | },
106 | };
107 |
108 | (fs.readFileSync as jest.Mock).mockReturnValue(
109 | JSON.stringify(mockCredentials)
110 | );
111 | (fs.existsSync as jest.Mock).mockReturnValue(true);
112 |
113 | const service = GoogleAuthService.getInstance();
114 | await service.initialize();
115 | await service.authenticate();
116 |
117 | expect(fs.existsSync).toHaveBeenCalled();
118 | });
119 |
120 | it("should throw error if OAuth2Client is not initialized", async () => {
121 | const service = GoogleAuthService.getInstance();
122 | await expect(service.authenticate()).rejects.toThrow(
123 | "Google authentication required"
124 | );
125 | });
126 |
127 | it("should throw error with instructions if no token exists", async () => {
128 | const mockCredentials = {
129 | installed: {
130 | client_id: "test-client-id",
131 | client_secret: "test-client-secret",
132 | redirect_uris: ["http://localhost"],
133 | },
134 | };
135 |
136 | (fs.readFileSync as jest.Mock).mockReturnValue(
137 | JSON.stringify(mockCredentials)
138 | );
139 | (fs.existsSync as jest.Mock).mockReturnValue(false);
140 |
141 | const service = GoogleAuthService.getInstance();
142 | await service.initialize();
143 | await expect(service.authenticate()).rejects.toThrow(
144 | "Google authentication required"
145 | );
146 | });
147 | });
148 |
149 | describe("getAuth", () => {
150 | it("should throw error if OAuth2Client is not initialized", () => {
151 | const service = GoogleAuthService.getInstance();
152 | service["oAuth2Client"] = null;
153 | expect(() => service.getAuth()).toThrow("OAuth2Client not initialized");
154 | });
155 |
156 | it("should return OAuth2Client if initialized", async () => {
157 | const service = GoogleAuthService.getInstance();
158 | const mockOAuth2Client = new OAuth2Client();
159 | service["oAuth2Client"] = mockOAuth2Client;
160 | expect(service.getAuth()).toBe(mockOAuth2Client);
161 | });
162 | });
163 |
164 | describe("saveToken", () => {
165 | it("should save token to file", async () => {
166 | const mockToken = { access_token: "test-token" };
167 | const service = GoogleAuthService.getInstance();
168 | await service.saveToken(mockToken);
169 |
170 | expect(fs.writeFileSync).toHaveBeenCalledWith(
171 | expect.any(String),
172 | JSON.stringify(mockToken)
173 | );
174 | });
175 | });
176 | });
177 |
```
--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ErrorObject } from "ajv";
2 | import type { JSONSchema7 } from "json-schema";
3 | import {Ajv} from "ajv";
4 |
5 | // Using type assertion to help TypeScript understand the constructor
6 | const ajv = new Ajv({
7 | allErrors: true,
8 | strict: false,
9 | strictSchema: false,
10 | strictTypes: false,
11 | });
12 |
13 | /**
14 | * Validates data against a schema and throws an error with details if validation fails
15 | */
16 | export function validateWithErrors(data: unknown, schema: JSONSchema7): void {
17 | const validate = ajv.compile(schema);
18 | const valid = validate(data);
19 |
20 | if (!valid) {
21 | const errors = validate.errors
22 | ?.map((e: ErrorObject) => {
23 | if (e.keyword === "required") {
24 | const property = (e.params as any).missingProperty;
25 | // Map property names to expected error messages
26 | const errorMap: Record<string, string> = {
27 | params: "Request must have params",
28 | messages: "Request must have at least one message",
29 | content: "Message must have a content object",
30 | text: "Text content must have a string text field",
31 | data: "Image content must have a base64 data field",
32 | mimeType: "Image content must have a mimeType field",
33 | type: "Message content must have a type field",
34 | title: "Missing required field: title",
35 | description: "Missing required field: description",
36 | static: "Missing required field: static instruction",
37 | dynamic: "Missing required field: dynamic instruction",
38 | state: "Missing required field: state",
39 | input_type: "Missing required field: input type",
40 | output_type: "Missing required field: output type",
41 | };
42 | return errorMap[property] || `Missing required field: ${property}`;
43 | }
44 | if (e.keyword === "minimum" && (e.params as any).limit === 1) {
45 | if ((e as any).instancePath === "/params/maxTokens") {
46 | return "maxTokens must be a positive number";
47 | }
48 | if ((e as any).instancePath === "/params/messages") {
49 | return "Request must have at least one message";
50 | }
51 | }
52 | if (e.keyword === "maximum" && (e.params as any).limit === 1) {
53 | if ((e as any).instancePath === "/params/temperature") {
54 | return "Temperature must be between 0 and 1";
55 | }
56 | if ((e as any).instancePath.includes("Priority")) {
57 | return "Priority values must be between 0 and 1";
58 | }
59 | }
60 | if (e.keyword === "enum") {
61 | if ((e as any).instancePath === "/params/includeContext") {
62 | return 'includeContext must be one of: "none", "thisServer", or "allServers"';
63 | }
64 | if ((e as any).instancePath.includes("/role")) {
65 | return 'Message role must be either "user" or "assistant"';
66 | }
67 | if ((e as any).instancePath.includes("/type")) {
68 | return 'Content type must be either "text" or "image"';
69 | }
70 | }
71 | if (e.keyword === "type") {
72 | if ((e as any).instancePath.includes("/text")) {
73 | return "Text content must be a string";
74 | }
75 | }
76 | if (
77 | e.keyword === "minItems" &&
78 | (e as any).instancePath === "/params/messages"
79 | ) {
80 | return "Request must have at least one message";
81 | }
82 | return e.message || "Unknown validation error";
83 | })
84 | .join(", ");
85 | throw new Error(errors);
86 | }
87 | }
88 |
89 | // Schema for validating sampling requests
90 | const samplingRequestSchema: JSONSchema7 = {
91 | type: "object",
92 | required: ["method", "params"],
93 | properties: {
94 | method: { type: "string", enum: ["sampling/createMessage"] },
95 | params: {
96 | type: "object",
97 | required: ["messages"],
98 | properties: {
99 | messages: {
100 | type: "array",
101 | minItems: 1,
102 | items: {
103 | type: "object",
104 | required: ["role", "content"],
105 | properties: {
106 | role: { type: "string", enum: ["user", "assistant"] },
107 | content: {
108 | oneOf: [
109 | {
110 | type: "object",
111 | required: ["type", "text"],
112 | properties: {
113 | type: { type: "string", enum: ["text"] },
114 | text: { type: "string" },
115 | },
116 | additionalProperties: false,
117 | },
118 | {
119 | type: "object",
120 | required: ["type", "data", "mimeType"],
121 | properties: {
122 | type: { type: "string", enum: ["image"] },
123 | data: { type: "string" },
124 | mimeType: { type: "string" },
125 | },
126 | additionalProperties: false,
127 | },
128 | ],
129 | },
130 | },
131 | },
132 | },
133 | maxTokens: { type: "number", minimum: 1 },
134 | temperature: { type: "number", minimum: 0, maximum: 1 },
135 | includeContext: {
136 | type: "string",
137 | enum: ["none", "thisServer", "allServers"],
138 | },
139 | modelPreferences: {
140 | type: "object",
141 | properties: {
142 | costPriority: { type: "number", minimum: 0, maximum: 1 },
143 | speedPriority: { type: "number", minimum: 0, maximum: 1 },
144 | intelligencePriority: { type: "number", minimum: 0, maximum: 1 },
145 | },
146 | },
147 | },
148 | },
149 | },
150 | };
151 |
152 | /**
153 | * Validates a request against a schema
154 | */
155 | export function validateRequest(request: unknown): void {
156 | validateWithErrors(request, samplingRequestSchema);
157 | }
158 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/prompt-handlers.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import type {
3 | GetPromptResult,
4 | ListPromptsResult,
5 | Prompt,
6 | } from "@modelcontextprotocol/sdk/types.js";
7 | import type { SystempromptPromptResponse } from "../../types/systemprompt.js";
8 | import { handleListPrompts, handleGetPrompt } from "../prompt-handlers.js";
9 | import { SystemPromptService } from "../../services/systemprompt-service.js";
10 |
11 | // Mock the constants modules
12 | jest.mock("../../constants/instructions.js", () => ({
13 | NOTION_PAGE_CREATOR_INSTRUCTIONS: "Test assistant instruction",
14 | NOTION_PAGE_EDITOR_INSTRUCTIONS: "Test editor instruction",
15 | }));
16 |
17 | const mockPrompts: SystempromptPromptResponse[] = [
18 | {
19 | id: "notion-page-creator",
20 | metadata: {
21 | title: "Notion Page Creator",
22 | description:
23 | "Generates a rich, detailed Notion page that expands upon basic inputs into comprehensive, well-structured content",
24 | created: "2024-01-01T00:00:00Z",
25 | updated: "2024-01-01T00:00:00Z",
26 | version: 1,
27 | status: "active",
28 | author: "test",
29 | log_message: "Initial version",
30 | tag: ["notion", "creator"],
31 | },
32 | instruction: {
33 | static: "Test assistant instruction",
34 | dynamic: "",
35 | state: "",
36 | },
37 | input: {
38 | name: "notion-page-creator-input",
39 | description: "Input parameters for creating a Notion page",
40 | type: ["object"],
41 | schema: {
42 | type: "object",
43 | properties: {
44 | databaseId: {
45 | type: "string",
46 | description: "The ID of the database to create the page in",
47 | },
48 | userInstructions: {
49 | type: "string",
50 | description: "Basic instructions or outline for the page content",
51 | },
52 | },
53 | required: ["databaseId", "userInstructions"],
54 | },
55 | },
56 | output: {
57 | name: "notion-page-creator-output",
58 | description: "Output format for the created Notion page",
59 | type: ["object"],
60 | schema: {
61 | type: "object",
62 | properties: {},
63 | required: ["parent", "properties"],
64 | },
65 | },
66 | _link: "https://api.systemprompt.io/v1/prompts/notion-page-creator",
67 | },
68 | ];
69 |
70 | type MockGetAllPromptsReturn = ReturnType<typeof SystemPromptService.prototype.getAllPrompts>;
71 |
72 | // Mock SystemPromptService
73 | jest.mock("../../services/systemprompt-service.js", () => {
74 | const mockGetAllPrompts = jest.fn(() => Promise.resolve(mockPrompts));
75 | const mockGetInstance = jest.fn(() => ({
76 | getAllPrompts: mockGetAllPrompts,
77 | }));
78 |
79 | return {
80 | SystemPromptService: {
81 | getInstance: mockGetInstance,
82 | initialize: jest.fn(),
83 | },
84 | };
85 | });
86 |
87 | describe("Prompt Handlers", () => {
88 | beforeEach(() => {
89 | jest.resetModules();
90 | jest.clearAllMocks();
91 | });
92 |
93 | describe("handleListPrompts", () => {
94 | it("should return a list of prompts", async () => {
95 | const result = await handleListPrompts({ method: "prompts/list" });
96 | expect(result.prompts).toBeDefined();
97 | expect(result.prompts[0].name).toBe(mockPrompts[0].metadata.title);
98 | });
99 | it("should handle errors gracefully", async () => {
100 | // Override mock for this specific test
101 | await jest.isolateModules(async () => {
102 | const mockError = new Error("Failed to fetch");
103 | const mockGetAllPrompts = jest.fn(() => Promise.reject(mockError));
104 | const mockGetInstance = jest.fn(() => ({
105 | getAllPrompts: mockGetAllPrompts,
106 | }));
107 |
108 | jest.doMock("../../services/systemprompt-service.js", () => ({
109 | SystemPromptService: {
110 | getInstance: mockGetInstance,
111 | },
112 | }));
113 |
114 | const { handleListPrompts } = await import("../prompt-handlers.js");
115 | await expect(handleListPrompts({ method: "prompts/list" })).rejects.toThrow(
116 | "Failed to fetch prompts from systemprompt.io",
117 | );
118 | });
119 | });
120 | });
121 |
122 | describe("handleGetPrompt", () => {
123 | it("should handle unknown prompts", async () => {
124 | await expect(
125 | handleGetPrompt({
126 | method: "prompts/get",
127 | params: { name: "Unknown Prompt" },
128 | }),
129 | ).rejects.toThrow("Prompt not found: Unknown Prompt");
130 | });
131 |
132 | it("should return the correct prompt", async () => {
133 | const result = await handleGetPrompt({
134 | method: "prompts/get",
135 | params: {
136 | name: "Notion Page Creator",
137 | arguments: {
138 | databaseId: "test-db-123",
139 | userInstructions: "Create a test page",
140 | },
141 | },
142 | });
143 |
144 | const prompt = result._meta?.prompt as SystempromptPromptResponse;
145 | expect(prompt).toBeDefined();
146 | expect(prompt.metadata.title).toBe("Notion Page Creator");
147 | expect(prompt.input.schema.properties).toHaveProperty("databaseId");
148 | expect(prompt.input.schema.properties).toHaveProperty("userInstructions");
149 | });
150 |
151 | it("should handle service errors with detailed messages", async () => {
152 | await jest.isolateModules(async () => {
153 | const mockError = new Error("Service unavailable");
154 | const mockGetAllPrompts = jest.fn(() => Promise.reject(mockError));
155 | const mockGetInstance = jest.fn(() => ({
156 | getAllPrompts: mockGetAllPrompts,
157 | }));
158 |
159 | jest.doMock("../../services/systemprompt-service.js", () => ({
160 | SystemPromptService: {
161 | getInstance: mockGetInstance,
162 | },
163 | }));
164 |
165 | const { handleGetPrompt } = await import("../prompt-handlers.js");
166 | await expect(
167 | handleGetPrompt({
168 | method: "prompts/get",
169 | params: { name: "Test Prompt" },
170 | }),
171 | ).rejects.toThrow("Failed to fetch prompt from systemprompt.io: Service unavailable");
172 | });
173 | });
174 |
175 | it("should handle errors without messages", async () => {
176 | await jest.isolateModules(async () => {
177 | const mockError = new Error();
178 | const mockGetAllPrompts = jest.fn(() => Promise.reject(mockError));
179 | const mockGetInstance = jest.fn(() => ({
180 | getAllPrompts: mockGetAllPrompts,
181 | }));
182 |
183 | jest.doMock("../../services/systemprompt-service.js", () => ({
184 | SystemPromptService: {
185 | getInstance: mockGetInstance,
186 | },
187 | }));
188 |
189 | const { handleGetPrompt } = await import("../prompt-handlers.js");
190 | await expect(
191 | handleGetPrompt({
192 | method: "prompts/get",
193 | params: { name: "Test Prompt" },
194 | }),
195 | ).rejects.toThrow("Failed to fetch prompt from systemprompt.io: Unknown error");
196 | });
197 | });
198 | });
199 | });
200 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/resource-handlers.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 | import {
3 | handleListResources,
4 | handleResourceCall,
5 | } from "../resource-handlers.js";
6 | import { SystemPromptService } from "../../services/systemprompt-service.js";
7 | import MockSystemPromptService from "../../__mocks__/systemprompt-service.js";
8 |
9 | jest.mock("../../services/systemprompt-service.js", () => {
10 | return {
11 | SystemPromptService: {
12 | initialize: (apiKey: string) => {
13 | const instance = MockSystemPromptService.getInstance();
14 | instance.initialize(apiKey);
15 | },
16 | getInstance: () => MockSystemPromptService.getInstance(),
17 | cleanup: () => {
18 | // No cleanup needed for mock
19 | },
20 | },
21 | };
22 | });
23 |
24 | describe("Resource Handlers", () => {
25 | beforeAll(() => {
26 | SystemPromptService.initialize("test-api-key");
27 | });
28 |
29 | afterAll(() => {
30 | SystemPromptService.cleanup();
31 | });
32 |
33 | describe("handleListResources", () => {
34 | it("should list the default agent resource", async () => {
35 | const result = await handleListResources({
36 | method: "resources/list",
37 | });
38 |
39 | expect(result.resources).toEqual([
40 | {
41 | uri: "resource:///block/default",
42 | name: "Systemprompt Agent",
43 | description:
44 | "An expert agent for managing and organizing content in workspaces",
45 | mimeType: "text/plain",
46 | },
47 | ]);
48 | expect(result._meta).toEqual({});
49 | });
50 |
51 | it("should handle service errors with messages", async () => {
52 | const mockError = new Error("Service unavailable");
53 | const mockListBlocks = jest.fn(() => Promise.reject(mockError));
54 | const mockGetInstance = jest.fn(
55 | () =>
56 | ({
57 | listBlocks: mockListBlocks,
58 | } as unknown as SystemPromptService)
59 | );
60 |
61 | jest
62 | .spyOn(SystemPromptService, "getInstance")
63 | .mockImplementation(mockGetInstance);
64 |
65 | await expect(
66 | handleListResources({ method: "resources/list" })
67 | ).rejects.toThrow(
68 | "Failed to fetch blocks from systemprompt.io: Service unavailable"
69 | );
70 |
71 | jest.restoreAllMocks();
72 | });
73 |
74 | it("should handle service errors without messages", async () => {
75 | const mockError = new Error();
76 | const mockListBlocks = jest.fn(() => Promise.reject(mockError));
77 | const mockGetInstance = jest.fn(
78 | () =>
79 | ({
80 | listBlocks: mockListBlocks,
81 | } as unknown as SystemPromptService)
82 | );
83 |
84 | jest
85 | .spyOn(SystemPromptService, "getInstance")
86 | .mockImplementation(mockGetInstance);
87 |
88 | await expect(
89 | handleListResources({ method: "resources/list" })
90 | ).rejects.toThrow(
91 | "Failed to fetch blocks from systemprompt.io: Unknown error"
92 | );
93 |
94 | jest.restoreAllMocks();
95 | });
96 | });
97 |
98 | describe("handleResourceCall", () => {
99 | it("should get the default agent resource", async () => {
100 | const result = await handleResourceCall({
101 | method: "resources/read",
102 | params: {
103 | uri: "resource:///block/default",
104 | },
105 | });
106 |
107 | const parsedContent = JSON.parse(result.contents[0].text as string) as {
108 | name: string;
109 | description: string;
110 | instruction: string;
111 | voice: string;
112 | config: {
113 | model: string;
114 | generationConfig: {
115 | responseModalities: string;
116 | speechConfig: {
117 | voiceConfig: {
118 | prebuiltVoiceConfig: {
119 | voiceName: string;
120 | };
121 | };
122 | };
123 | };
124 | };
125 | };
126 |
127 | expect(result.contents[0].uri).toBe("resource:///block/default");
128 | expect(result.contents[0].mimeType).toBe("text/plain");
129 | expect(parsedContent).toEqual({
130 | name: "Systemprompt Agent",
131 | description:
132 | "An expert agent for managing and organizing content in workspaces",
133 | instruction: "You are a specialized agent",
134 | voice: "Kore",
135 | config: {
136 | model: "models/gemini-2.0-flash-exp",
137 | generationConfig: {
138 | responseModalities: "audio",
139 | speechConfig: {
140 | voiceConfig: {
141 | prebuiltVoiceConfig: {
142 | voiceName: "Kore",
143 | },
144 | },
145 | },
146 | },
147 | },
148 | });
149 | expect(result._meta).toEqual({});
150 | });
151 |
152 | it("should handle invalid URI format", async () => {
153 | await expect(
154 | handleResourceCall({
155 | method: "resources/read",
156 | params: {
157 | uri: "invalid-uri",
158 | },
159 | })
160 | ).rejects.toThrow(
161 | "Invalid resource URI format - expected resource:///block/{id}"
162 | );
163 | });
164 |
165 | it("should handle non-default resource request", async () => {
166 | await expect(
167 | handleResourceCall({
168 | method: "resources/read",
169 | params: {
170 | uri: "resource:///block/nonexistent",
171 | },
172 | })
173 | ).rejects.toThrow(
174 | "Failed to fetch block from systemprompt.io: Resource not found"
175 | );
176 | });
177 |
178 | it("should handle service errors with messages", async () => {
179 | const mockError = new Error("Service unavailable");
180 | const mockGetBlock = jest.fn(() => Promise.reject(mockError));
181 | const mockGetInstance = jest.fn(
182 | () =>
183 | ({
184 | getBlock: mockGetBlock,
185 | } as unknown as SystemPromptService)
186 | );
187 |
188 | jest
189 | .spyOn(SystemPromptService, "getInstance")
190 | .mockImplementation(mockGetInstance);
191 |
192 | await expect(
193 | handleResourceCall({
194 | method: "resources/read",
195 | params: { uri: "resource:///block/test" },
196 | })
197 | ).rejects.toThrow(
198 | "Failed to fetch block from systemprompt.io: Service unavailable"
199 | );
200 |
201 | jest.restoreAllMocks();
202 | });
203 |
204 | it("should handle service errors without messages", async () => {
205 | const mockError = new Error();
206 | const mockGetBlock = jest.fn(() => Promise.reject(mockError));
207 | const mockGetInstance = jest.fn(
208 | () =>
209 | ({
210 | getBlock: mockGetBlock,
211 | } as unknown as SystemPromptService)
212 | );
213 |
214 | jest
215 | .spyOn(SystemPromptService, "getInstance")
216 | .mockImplementation(mockGetInstance);
217 |
218 | await expect(
219 | handleResourceCall({
220 | method: "resources/read",
221 | params: { uri: "resource:///block/test" },
222 | })
223 | ).rejects.toThrow(
224 | "Failed to fetch block from systemprompt.io: Unknown error"
225 | );
226 |
227 | jest.restoreAllMocks();
228 | });
229 | });
230 | });
231 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/callbacks.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Mock setup
2 | jest.mock("../../server", () => ({
3 | server: {
4 | notification: jest.fn(),
5 | },
6 | }));
7 |
8 | jest.mock("../../services/systemprompt-service");
9 |
10 | import {
11 | handleCreatePromptCallback,
12 | handleEditPromptCallback,
13 | handleCreateBlockCallback,
14 | handleEditBlockCallback,
15 | handleCreateAgentCallback,
16 | handleEditAgentCallback,
17 | } from "../callbacks";
18 | import type { CreateMessageResult } from "@modelcontextprotocol/sdk/types.js";
19 | import { SystemPromptService } from "../../services/systemprompt-service";
20 | import type { SystempromptPromptResponse } from "../../types/systemprompt";
21 |
22 | // Mock response data
23 | const mockPromptResponse: SystempromptPromptResponse = {
24 | id: "test-id",
25 | metadata: {
26 | title: "Test",
27 | description: null,
28 | created: "2025-01-23T09:55:32.932Z",
29 | updated: "2025-01-23T09:55:32.932Z",
30 | version: 1,
31 | status: "active",
32 | author: "test",
33 | log_message: "test",
34 | tag: ["test"],
35 | },
36 | instruction: {
37 | static: "test",
38 | dynamic: "test",
39 | state: "test",
40 | },
41 | input: {
42 | name: "test",
43 | description: "test",
44 | type: ["text"],
45 | schema: {},
46 | },
47 | output: {
48 | name: "test",
49 | description: "test",
50 | type: ["text"],
51 | schema: {},
52 | },
53 | _link: "test",
54 | };
55 |
56 | // Mock the SystemPromptService class
57 | const mockCreatePrompt = jest.fn().mockResolvedValue(mockPromptResponse);
58 | const mockEditPrompt = jest.fn().mockResolvedValue(mockPromptResponse);
59 | const mockCreateBlock = jest.fn().mockResolvedValue(mockPromptResponse);
60 | const mockEditBlock = jest.fn().mockResolvedValue(mockPromptResponse);
61 | const mockCreateAgent = jest.fn().mockResolvedValue(mockPromptResponse);
62 | const mockEditAgent = jest.fn().mockResolvedValue(mockPromptResponse);
63 |
64 | describe("callbacks", () => {
65 | beforeEach(() => {
66 | jest.clearAllMocks();
67 | // Setup SystemPromptService mock
68 | (SystemPromptService.getInstance as jest.Mock).mockReturnValue({
69 | createPrompt: mockCreatePrompt,
70 | editPrompt: mockEditPrompt,
71 | createBlock: mockCreateBlock,
72 | editBlock: mockEditBlock,
73 | createAgent: mockCreateAgent,
74 | editAgent: mockEditAgent,
75 | getAllPrompts: jest.fn().mockResolvedValue([]),
76 | listBlocks: jest.fn().mockResolvedValue([]),
77 | });
78 | });
79 |
80 | it("should handle create prompt callback", async () => {
81 | const response: CreateMessageResult = {
82 | content: {
83 | type: "text",
84 | text: JSON.stringify(mockPromptResponse),
85 | },
86 | role: "assistant",
87 | model: "test-model",
88 | _meta: {},
89 | };
90 |
91 | const result = await handleCreatePromptCallback(response);
92 | expect(mockCreatePrompt).toHaveBeenCalled();
93 | expect(response.content.type).toBe("text");
94 | if (response.content.type === "text") {
95 | expect(JSON.parse(response.content.text)).toEqual(mockPromptResponse);
96 | }
97 | });
98 |
99 | it("should handle edit prompt callback", async () => {
100 | const response: CreateMessageResult = {
101 | content: {
102 | type: "text",
103 | text: JSON.stringify(mockPromptResponse),
104 | },
105 | role: "assistant",
106 | model: "test-model",
107 | _meta: {},
108 | };
109 |
110 | const result = await handleEditPromptCallback(response);
111 | expect(mockEditPrompt).toHaveBeenCalled();
112 | expect(response.content.type).toBe("text");
113 | if (response.content.type === "text") {
114 | expect(JSON.parse(response.content.text)).toEqual(mockPromptResponse);
115 | }
116 | });
117 |
118 | it("should handle create block callback", async () => {
119 | const response: CreateMessageResult = {
120 | content: {
121 | type: "text",
122 | text: JSON.stringify(mockPromptResponse),
123 | },
124 | role: "assistant",
125 | model: "test-model",
126 | _meta: {},
127 | };
128 |
129 | const result = await handleCreateBlockCallback(response);
130 | expect(response.content.type).toBe("text");
131 | if (response.content.type === "text") {
132 | const responseData = JSON.parse(response.content.text);
133 | expect(responseData).toEqual(mockPromptResponse);
134 | }
135 | expect(mockCreateBlock).toHaveBeenCalled();
136 | });
137 |
138 | it("should handle edit block callback", async () => {
139 | const response: CreateMessageResult = {
140 | content: {
141 | type: "text",
142 | text: JSON.stringify(mockPromptResponse),
143 | },
144 | role: "assistant",
145 | model: "test-model",
146 | _meta: {},
147 | };
148 |
149 | const result = await handleEditBlockCallback(response);
150 | expect(response.content.type).toBe("text");
151 | if (response.content.type === "text") {
152 | const responseData = JSON.parse(response.content.text);
153 | expect(responseData).toEqual(mockPromptResponse);
154 | }
155 | expect(mockEditBlock).toHaveBeenCalled();
156 | });
157 |
158 | it("should handle create agent callback", async () => {
159 | const response: CreateMessageResult = {
160 | content: {
161 | type: "text",
162 | text: JSON.stringify(mockPromptResponse),
163 | },
164 | role: "assistant",
165 | model: "test-model",
166 | _meta: {},
167 | };
168 |
169 | const result = await handleCreateAgentCallback(response);
170 | expect(response.content.type).toBe("text");
171 | if (response.content.type === "text") {
172 | const responseData = JSON.parse(response.content.text);
173 | expect(responseData).toEqual(mockPromptResponse);
174 | }
175 | expect(mockCreateAgent).toHaveBeenCalled();
176 | });
177 |
178 | it("should handle edit agent callback", async () => {
179 | const response: CreateMessageResult = {
180 | content: {
181 | type: "text",
182 | text: JSON.stringify(mockPromptResponse),
183 | },
184 | role: "assistant",
185 | model: "test-model",
186 | _meta: {},
187 | };
188 |
189 | const result = await handleEditAgentCallback(response);
190 | expect(response.content.type).toBe("text");
191 | if (response.content.type === "text") {
192 | const responseData = JSON.parse(response.content.text);
193 | expect(responseData).toEqual(mockPromptResponse);
194 | }
195 | expect(mockEditAgent).toHaveBeenCalled();
196 | });
197 |
198 | it("should handle non-text content error", async () => {
199 | const result: CreateMessageResult = {
200 | content: {
201 | type: "image" as const,
202 | data: "base64data",
203 | mimeType: "image/jpeg",
204 | },
205 | role: "assistant" as const,
206 | model: "test-model",
207 | _meta: {},
208 | };
209 |
210 | await expect(handleCreatePromptCallback(result)).rejects.toThrow("Expected text content");
211 | await expect(handleEditPromptCallback(result)).rejects.toThrow("Expected text content");
212 | await expect(handleCreateBlockCallback(result)).rejects.toThrow("Expected text content");
213 | await expect(handleEditBlockCallback(result)).rejects.toThrow("Expected text content");
214 | await expect(handleCreateAgentCallback(result)).rejects.toThrow("Expected text content");
215 | await expect(handleEditAgentCallback(result)).rejects.toThrow("Expected text content");
216 | });
217 | });
218 |
```
--------------------------------------------------------------------------------
/src/handlers/__tests__/tool-handlers.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest, describe, it, expect, beforeEach } from "@jest/globals";
2 | import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
3 | import { handleToolCall } from "../tool-handlers.js";
4 | import type {
5 | SystempromptPromptResponse,
6 | SystempromptUserStatusResponse,
7 | SystempromptBlockResponse,
8 | SystempromptAgentResponse,
9 | } from "../../types/systemprompt.js";
10 |
11 | // Mock the server config
12 | jest.mock("../../config/server-config.js", () => ({
13 | serverConfig: {
14 | port: 3000,
15 | host: "localhost",
16 | },
17 | serverCapabilities: {
18 | tools: [],
19 | },
20 | }));
21 |
22 | // Mock the main function
23 | jest.mock("../../index.ts", () => ({
24 | main: jest.fn(),
25 | server: {
26 | notification: jest.fn().mockImplementation(async () => {}),
27 | },
28 | }));
29 |
30 | // Mock the sampling module
31 | jest.mock("../sampling.js", () => ({
32 | sendSamplingRequest: jest.fn().mockImplementation(async () => ({
33 | content: [
34 | {
35 | type: "text",
36 | text: JSON.stringify({ metadata: { title: "Test", description: "Test" }, content: "Test" }),
37 | },
38 | ],
39 | })),
40 | }));
41 |
42 | // Mock SystemPromptService
43 | const mockUserStatus: SystempromptUserStatusResponse = {
44 | user: {
45 | id: "user123",
46 | uuid: "uuid123",
47 | name: "Test User",
48 | email: "[email protected]",
49 | roles: ["user"],
50 | paddle_id: "paddle123",
51 | },
52 | content: {
53 | prompt: 0,
54 | artifact: 0,
55 | block: 0,
56 | conversation: 0,
57 | },
58 | usage: {
59 | ai: {
60 | execution: 0,
61 | token: 0,
62 | },
63 | api: {
64 | generation: 0,
65 | },
66 | },
67 | billing: {
68 | customer: {
69 | id: "cust123",
70 | name: null,
71 | email: "[email protected]",
72 | marketingConsent: false,
73 | status: "active",
74 | customData: null,
75 | locale: "en",
76 | createdAt: {
77 | date: "2024-01-01T00:00:00Z",
78 | timezone_type: 3,
79 | timezone: "UTC",
80 | },
81 | updatedAt: {
82 | date: "2024-01-01T00:00:00Z",
83 | timezone_type: 3,
84 | timezone: "UTC",
85 | },
86 | importMeta: null,
87 | },
88 | subscription: [
89 | {
90 | id: "sub123",
91 | status: "active",
92 | currency_code: "USD",
93 | billing_cycle: {
94 | frequency: 1,
95 | interval: "month",
96 | },
97 | current_billing_period: {
98 | starts_at: "2024-01-01",
99 | ends_at: "2024-02-01",
100 | },
101 | items: [
102 | {
103 | product: { name: "Test Product" },
104 | price: {
105 | unit_price: { amount: "10.00", currency_code: "USD" },
106 | },
107 | },
108 | ],
109 | },
110 | ],
111 | },
112 | api_key: "test-api-key",
113 | };
114 |
115 | interface MockSystemPromptService {
116 | fetchUserStatus: () => Promise<SystempromptUserStatusResponse>;
117 | getAllPrompts: () => Promise<SystempromptPromptResponse[]>;
118 | listBlocks: () => Promise<SystempromptBlockResponse[]>;
119 | listAgents: () => Promise<SystempromptAgentResponse[]>;
120 | deletePrompt: (id: string) => Promise<void>;
121 | deleteBlock: (id: string) => Promise<void>;
122 | }
123 |
124 | const mockSystemPromptService = {
125 | fetchUserStatus: jest
126 | .fn<() => Promise<SystempromptUserStatusResponse>>()
127 | .mockResolvedValue(mockUserStatus),
128 | getAllPrompts: jest.fn<() => Promise<SystempromptPromptResponse[]>>().mockResolvedValue([]),
129 | listBlocks: jest.fn<() => Promise<SystempromptBlockResponse[]>>().mockResolvedValue([]),
130 | listAgents: jest.fn<() => Promise<SystempromptAgentResponse[]>>().mockResolvedValue([]),
131 | deletePrompt: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
132 | deleteBlock: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
133 | };
134 |
135 | jest.mock("../../services/systemprompt-service.js", () => ({
136 | SystemPromptService: jest.fn(() => mockSystemPromptService),
137 | }));
138 |
139 | describe("Tool Handlers", () => {
140 | beforeEach(() => {
141 | jest.clearAllMocks();
142 | });
143 |
144 | describe("handleToolCall", () => {
145 | describe("Heartbeat", () => {
146 | it("should handle systemprompt_heartbeat", async () => {
147 | const request: CallToolRequest = {
148 | method: "tools/call",
149 | params: {
150 | name: "systemprompt_heartbeat",
151 | params: {},
152 | },
153 | };
154 |
155 | const result = await handleToolCall(request);
156 | expect(mockSystemPromptService.fetchUserStatus).toHaveBeenCalled();
157 | expect(result.content[0].type).toBe("text");
158 | expect(result.content[0].text).toContain("User Information");
159 | });
160 |
161 | it("should handle errors gracefully", async () => {
162 | const error = new Error("Failed to fetch user status");
163 | mockSystemPromptService.fetchUserStatus.mockRejectedValueOnce(error);
164 | const request: CallToolRequest = {
165 | method: "tools/call",
166 | params: {
167 | name: "systemprompt_heartbeat",
168 | params: {},
169 | },
170 | };
171 | await expect(handleToolCall(request)).rejects.toThrow("Failed to fetch user status");
172 | });
173 | });
174 |
175 | describe("Resource Operations", () => {
176 | it("should handle systemprompt_fetch_resources", async () => {
177 | const result = await handleToolCall({
178 | method: "tools/call",
179 | params: {
180 | name: "systemprompt_fetch_resources",
181 | params: {},
182 | },
183 | });
184 | expect(mockSystemPromptService.getAllPrompts).toHaveBeenCalled();
185 | expect(mockSystemPromptService.listBlocks).toHaveBeenCalled();
186 | expect(mockSystemPromptService.listAgents).toHaveBeenCalled();
187 | expect(result.content[0].type).toBe("text");
188 | expect(result.content[0].text).toContain("Resources");
189 | });
190 |
191 | it("should handle delete resource failure", async () => {
192 | mockSystemPromptService.deletePrompt.mockRejectedValueOnce(
193 | new Error("Failed to delete prompt"),
194 | );
195 | mockSystemPromptService.deleteBlock.mockRejectedValueOnce(
196 | new Error("Failed to delete block"),
197 | );
198 | await expect(
199 | handleToolCall({
200 | method: "tools/call",
201 | params: {
202 | name: "systemprompt_delete_resource",
203 | arguments: {
204 | id: "nonexistent123",
205 | },
206 | },
207 | }),
208 | ).rejects.toThrow("Failed to delete resource with ID nonexistent123");
209 | });
210 | });
211 |
212 | describe("Error Handling", () => {
213 | it("should handle invalid tool name", async () => {
214 | await expect(
215 | handleToolCall({
216 | method: "tools/call",
217 | params: {
218 | name: "invalid_tool",
219 | params: {},
220 | },
221 | }),
222 | ).rejects.toThrow("Unknown tool: invalid_tool");
223 | });
224 |
225 | it("should handle service errors", async () => {
226 | mockSystemPromptService.fetchUserStatus.mockRejectedValueOnce(new Error("Service error"));
227 | await expect(
228 | handleToolCall({
229 | method: "tools/call",
230 | params: {
231 | name: "systemprompt_heartbeat",
232 | params: {},
233 | },
234 | }),
235 | ).rejects.toThrow("Service error");
236 | });
237 | });
238 | });
239 | });
240 |
```