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