diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/README.md b/clients/Gradio/mcp_servers/pgpt-mcp-server/README.md new file mode 100644 index 0000000..a403c21 --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/README.md @@ -0,0 +1,382 @@ +# PrivateGPT MCP Server + +A Model Context Protocol (MCP) server implementation that allows you to use PrivateGPT as an agent for your preferred MCP client. This enables seamless integration between PrivateGPT's powerful capabilities and any MCP-compatible application. + +> Maintained by [elswa-dev](https://github.com/elswa-dev) + +## What is MCP? + +MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools. + +### Why MCP? + +MCP helps you build agents and complex workflows on top of LLMs. LLMs frequently need to integrate with data and tools, and MCP provides: +- A growing list of pre-built integrations that your LLM can directly plug into +- The flexibility to switch between LLM providers and vendors +- Best practices for securing your data within your infrastructure + +### How it Works + +At its core, MCP follows a client-server architecture where a host application can connect to multiple servers: + +- **MCP Hosts**: Programs like Claude Desktop, IDEs, or AI tools that want to access data through MCP +- **MCP Clients**: Protocol clients that maintain 1:1 connections with servers +- **MCP Servers**: Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol +- **Local Data Sources**: Your computer's files, databases, and services that MCP servers can securely access +- **Remote Services**: External systems available over the internet (e.g., through APIs) that MCP servers can connect to + +## Overview + +This server provides a bridge between MCP clients and the PrivateGPT API, allowing you to: +- Chat with PrivateGPT using both public and private knowledge bases +- Create and manage knowledge sources +- Organize sources into groups +- Control access through group-based permissions + +## Features + +### Authentication +- Secure login using email/password credentials +- Automatic token management +- Bearer token authentication for all API requests +- Logout functionality to invalidate tokens + +### Chat Capabilities +- Start new chat conversations +- Continue existing chat conversations +- Get chat information and history +- Use public knowledge base +- Use group-specific knowledge bases +- Multi-language support +- Context-aware responses + +### Source Management +- Create new sources with markdown formatting +- Edit existing sources (name, content, groups) +- Delete sources +- Assign sources to groups +- List sources by group +- Get source details and metadata +- Track source states (creation → vectorized) + +### Group Management +- List personal groups +- List assignable groups +- Create new groups +- Delete existing groups +- Group-based access control +- Personal workspace isolation + +### User Management +- Create new users with full configuration +- Edit existing users (all properties) +- Delete users +- Role-based permissions +- FTP access management +- Language and timezone preferences + +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/Fujitsu-AI/MCP-Server-for-MAS-Developments.git +cd MCP-Server-for-MAS-Developments/clients/Gradio/mcp_servers/pgpt-mcp-server +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Build the project: +```bash +npm run build +``` + +## Configuration + +Create a `.env` file with your PrivateGPT credentials: +```env +PRIVATE_GPT_API_URL=http://your-privategpt-instance/api/v1 +user=your.email@example.com +password=your_password +``` + +## MCP Client Setup + +### Prerequisites +- Node.js 18 or higher installed +- A running instance of PrivateGPT with API access +- An MCP-compatible client application + +### Setup Steps +1. Build the project as described in the Installation section above + +2. Configure your MCP client to use this server. The exact configuration depends on your client, but typically involves: + - Specifying the runtime as `node` + - Pointing to the compiled server at `dist/index.js` + - Setting environment variables for PrivateGPT connection + +### Example Configuration (Claude Desktop) +```json +{ + "mcpServers": { + "pgpt": { + "runtime": "node", + "command": "node", + "args": ["path/to/pgpt-mcp-server/dist/index.js"], + "stdio": "pipe", + "env": { + "PRIVATE_GPT_API_URL": "https://your-privategpt-instance/api/v1", + "user": "your.email@example.com", + "password": "your_password", + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } + } + } +} +``` + +### Environment Variables +- `PRIVATE_GPT_API_URL`: Your PrivateGPT API endpoint +- `user`: Your PrivateGPT username/email +- `password`: Your PrivateGPT password +- `NODE_TLS_REJECT_UNAUTHORIZED`: Set to "0" for self-signed certificates (development only) + +### Troubleshooting +- Verify the server path points to the compiled `dist/index.js` file +- Check that Node.js is accessible from your system PATH +- Ensure the PrivateGPT instance is accessible from your machine +- Verify environment variables are correctly set +- Check your MCP client logs for connection errors + +## Usage + +Start the server: +```bash +node dist/index.js +``` + +The server will start and listen on stdio for MCP commands. + +## Available Tools + +### chat +Start or continue a chat with PrivateGPT. +```typescript +{ + question: string; // The question or prompt to send + usePublic?: boolean; // Whether to use public knowledge base + groups?: string[]; // Group names to use for RAG + language?: string; // Language code (e.g., "en") +} +``` + +### create_source +Create a new source with automatic markdown formatting. +```typescript +{ + name: string; // Name of the source + content: string; // Content to be formatted as markdown + groups?: string[]; // Optional groups to assign the source to +} +``` + +### list_groups +Get available personal and assignable groups. +```typescript +{} // No parameters required +``` + +### list_sources +List all sources in a specific group. +```typescript +{ + groupName: string; // Name of the group to list sources from +} +``` + +### get_source +Get information about a specific source. +```typescript +{ + sourceId: string; // ID of the source to retrieve +} +``` + +### continue_chat +Continue an existing chat conversation. +```typescript +{ + chatId: string; // ID of the chat to continue + question: string; // The question or prompt to send +} +``` + +### get_chat +Get information about an existing chat. +```typescript +{ + chatId: string; // ID of the chat to retrieve +} +``` + +### edit_source +Edit an existing source. +```typescript +{ + sourceId: string; // ID of the source to edit + name?: string; // New name for the source (optional) + content?: string; // New content for the source (optional) + groups?: string[]; // New groups for the source (optional) +} +``` + +### delete_source +Delete an existing source. +```typescript +{ + sourceId: string; // ID of the source to delete +} +``` + +### create_group +Create a new group. +```typescript +{ + groupName: string; // Name of the group to create +} +``` + +### delete_group +Delete an existing group. +```typescript +{ + groupName: string; // Name of the group to delete +} +``` + +### create_user +Create a new user. +```typescript +{ + name: string; // Full name of the user + email: string; // Email address of the user + password: string; // Password for the user + language?: string; // Language preference (optional, defaults to "en") + timezone?: string; // Timezone preference (optional, defaults to "Europe/Berlin") + usePublic: boolean; // Whether user can use public knowledge base + groups: string[]; // Groups to assign to the user + roles: string[]; // Roles to assign to the user + activateFtp?: boolean; // Whether to activate FTP access (optional) + ftpPassword?: string; // FTP password (optional, required if activateFtp is true) +} +``` + +### edit_user +Edit an existing user. +```typescript +{ + email: string; // Email address of the user to edit (required) + name?: string; // New full name (optional) + password?: string; // New password (optional) + language?: string; // New language preference (optional) + timezone?: string; // New timezone preference (optional) + publicUpload?: boolean; // Whether user can upload to public (optional) + groups?: string[]; // New groups for the user (optional) + roles?: string[]; // New roles for the user (optional) + activateFtp?: boolean; // Whether to activate FTP access (optional) + ftpPassword?: string; // New FTP password (optional) +} +``` + +### delete_user +Delete an existing user. +```typescript +{ + email: string; // Email address of the user to delete +} +``` + +### logout +Logout and invalidate the current API token. +```typescript +{} // No parameters required +``` + +## API Integration Details + +### Authentication Flow +1. Server starts with email/password credentials +2. First request triggers login to get Bearer token +3. Token is cached and reused for subsequent requests +4. All API calls use Bearer authentication + +### Response Handling +- Success responses include data, message, and status +- Error responses are mapped to appropriate MCP error codes +- Detailed error messages are preserved +- Debug logging helps troubleshoot issues + +### Security +- SSL certificate validation (disabled in development) +- Secure credential handling +- Token-based authentication +- Group-based access control + +## Development + +### Building +```bash +npm run build +``` + +### Type Checking +```bash +npm run type-check +``` + +### Linting +```bash +npm run lint +``` + +### Testing +```bash +npm test +``` + +## Project Structure + +``` +src/ + ├── index.ts # Main server implementation + ├── types/ # TypeScript type definitions + │ └── api.ts # API interface types + └── services/ # Service implementations + └── pgpt-service.ts # PrivateGPT API service +``` + +## Error Handling + +The server handles various error scenarios: +- Authentication failures +- Network errors +- Invalid requests +- API errors +- Rate limiting +- Timeout errors + +Errors are mapped to appropriate MCP error codes and include detailed messages for debugging. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/package.json b/clients/Gradio/mcp_servers/pgpt-mcp-server/package.json new file mode 100644 index 0000000..32141ec --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/package.json @@ -0,0 +1,46 @@ +{ + "name": "pgpt-mcp-server", + "version": "1.4.0", + "description": "PrivateGPT MCP Server", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", + "start": "node dist/index.js", + "dev": "ts-node --esm src/index.ts", + "test": "jest", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"" + }, + "keywords": [ + "mcp", + "privategpt" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.6.2", + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/content-type": "^1.1.8", + "@types/jest": "^29.5.10", + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/parser": "^6.12.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "jest": "^29.7.0", + "prettier": "^3.1.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.3.2" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/src/index.ts b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/index.ts new file mode 100644 index 0000000..53b97c9 --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/index.ts @@ -0,0 +1,799 @@ +#!/usr/bin/env node +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ErrorCode, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + McpError, + ReadResourceRequestSchema, + Request +} from '@modelcontextprotocol/sdk/types.js'; +import axios, { AxiosError } from 'axios'; +import https from 'https'; + +// Assert environment variables are defined +let API_URL = process.env.PRIVATE_GPT_API_URL as string; +const USER = process.env.user as string; +const PASSWORD = process.env.password as string; + +// Use environment variables for proxy configuration +const PROXY_USER = process.env.PROXY_USER as string; +const PROXY_PASSWORD = process.env.PROXY_PASSWORD as string; + +// Validate proxy credentials if provided +if (PROXY_USER && !PROXY_PASSWORD) { + throw new Error('PROXY_PASSWORD environment variable is required when PROXY_USER is set'); +} +if (PROXY_PASSWORD && !PROXY_USER) { + throw new Error('PROXY_USER environment variable is required when PROXY_PASSWORD is set'); +} + +// Add /api/v1 prefix if not present +if (API_URL && !API_URL.endsWith('/api/v1')) { + API_URL = API_URL.replace(/\/?$/, '/api/v1'); +} + +// Validate required environment variables +if (!API_URL) { + throw new Error('PRIVATE_GPT_API_URL environment variable is required'); +} + +if (!USER || !PASSWORD) { + throw new Error('user and password environment variables are required'); +} + +console.error('Starting server with config:', { + API_URL, + USER, + PROXY_USER: PROXY_USER ? '***' : 'not set' +}); + +class PrivateGPTServer { + private server: Server; + private axiosInstance; + private authToken: string | null = null; + + constructor() { + this.server = new Server( + { + name: 'pgpt-mcp-server', + version: '0.2', + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + } + ); + + // Create axios instance with optional proxy auth + const headers: Record = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + + // Add proxy authentication if credentials are provided + if (PROXY_USER && PROXY_PASSWORD) { + const proxyAuth = Buffer.from(`${PROXY_USER}:${PROXY_PASSWORD}`).toString('base64'); + headers['Authorization'] = `Basic ${proxyAuth}`; + } + + this.axiosInstance = axios.create({ + baseURL: API_URL, + headers, + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }) + }); + + this.setupResourceHandlers(); + this.setupToolHandlers(); + + // Error handling + this.server.onerror = (error: Error) => console.error('[MCP Error]', error); + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + private async ensureAuthenticated() { + if (!this.authToken) { + console.error('Getting auth token...'); + try { + const loginResponse = await this.axiosInstance.post('/login', { + email: USER, + password: PASSWORD + }); + console.error('Login response:', loginResponse.data); + this.authToken = loginResponse.data.data.token; + console.error('Got auth token'); + + // Update authorization header with combined auth + let combinedAuth = `Bearer ${this.authToken}`; + if (PROXY_USER && PROXY_PASSWORD) { + const proxyAuth = Buffer.from(`${PROXY_USER}:${PROXY_PASSWORD}`).toString('base64'); + combinedAuth = `Basic ${proxyAuth}, Bearer ${this.authToken}`; + } + + // Set on multiple places to ensure it's used + this.axiosInstance.defaults.headers.common['Authorization'] = combinedAuth; + this.axiosInstance.defaults.headers['Authorization'] = combinedAuth; + + console.error('Updated Authorization header'); + } catch (error) { + console.error('Login error:', error); + throw error; + } + } + } + + private setupResourceHandlers() { + // List available resources + this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [] + })); + + // List resource templates + this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ + resourceTemplates: [] + })); + + // Read resource + this.server.setRequestHandler(ReadResourceRequestSchema, async (request: Request) => { + if (!request.params?.uri) { + throw new McpError(ErrorCode.InvalidRequest, 'Missing URI parameter'); + } + throw new McpError(ErrorCode.InvalidRequest, `Invalid URI: ${request.params.uri}`); + }); + } + + private setupToolHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'chat', + description: 'Start or continue a chat with PrivateGPT with optional RAG capabilities', + inputSchema: { + type: 'object', + properties: { + question: { + type: 'string', + description: 'The question or prompt to send' + }, + usePublic: { + type: 'boolean', + description: 'Whether to use public knowledge base', + default: false + }, + groups: { + type: 'array', + items: { + type: 'string' + }, + description: 'Group names to use for RAG (mutually exclusive with usePublic)' + }, + language: { + type: 'string', + description: 'Language code (e.g., "en" for English)', + default: 'en' + } + }, + required: ['question'] + } + }, + { + name: 'create_source', + description: 'Create a new source with automatic markdown formatting', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the source' + }, + content: { + type: 'string', + description: 'Content to be formatted as markdown' + }, + groups: { + type: 'array', + items: { + type: 'string' + }, + description: 'Optional groups to assign the source to' + } + }, + required: ['name', 'content'] + } + }, + { + name: 'list_groups', + description: 'Get available personal and assignable groups', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'list_sources', + description: 'List all sources in a specific group', + inputSchema: { + type: 'object', + properties: { + groupName: { + type: 'string', + description: 'Name of the group to list sources from' + } + }, + required: ['groupName'] + } + }, + { + name: 'get_source', + description: 'Get information about a specific source', + inputSchema: { + type: 'object', + properties: { + sourceId: { + type: 'string', + description: 'ID of the source to retrieve' + } + }, + required: ['sourceId'] + } + }, + { + name: 'continue_chat', + description: 'Continue an existing chat conversation', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'ID of the chat to continue' + }, + question: { + type: 'string', + description: 'The question or prompt to send' + } + }, + required: ['chatId', 'question'] + } + }, + { + name: 'get_chat', + description: 'Get information about an existing chat', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'ID of the chat to retrieve' + } + }, + required: ['chatId'] + } + }, + { + name: 'edit_source', + description: 'Edit an existing source', + inputSchema: { + type: 'object', + properties: { + sourceId: { + type: 'string', + description: 'ID of the source to edit' + }, + name: { + type: 'string', + description: 'New name for the source (optional)' + }, + content: { + type: 'string', + description: 'New content for the source (optional)' + }, + groups: { + type: 'array', + items: { + type: 'string' + }, + description: 'New groups for the source (optional)' + } + }, + required: ['sourceId'] + } + }, + { + name: 'delete_source', + description: 'Delete an existing source', + inputSchema: { + type: 'object', + properties: { + sourceId: { + type: 'string', + description: 'ID of the source to delete' + } + }, + required: ['sourceId'] + } + }, + { + name: 'create_group', + description: 'Create a new group', + inputSchema: { + type: 'object', + properties: { + groupName: { + type: 'string', + description: 'Name of the group to create' + } + }, + required: ['groupName'] + } + }, + { + name: 'delete_group', + description: 'Delete an existing group', + inputSchema: { + type: 'object', + properties: { + groupName: { + type: 'string', + description: 'Name of the group to delete' + } + }, + required: ['groupName'] + } + }, + { + name: 'create_user', + description: 'Create a new user', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Full name of the user' + }, + email: { + type: 'string', + description: 'Email address of the user' + }, + password: { + type: 'string', + description: 'Password for the user' + }, + language: { + type: 'string', + description: 'Language preference (optional, defaults to "en")' + }, + timezone: { + type: 'string', + description: 'Timezone preference (optional, defaults to "Europe/Berlin")' + }, + usePublic: { + type: 'boolean', + description: 'Whether user can use public knowledge base' + }, + groups: { + type: 'array', + items: { + type: 'string' + }, + description: 'Groups to assign to the user' + }, + roles: { + type: 'array', + items: { + type: 'string' + }, + description: 'Roles to assign to the user' + }, + activateFtp: { + type: 'boolean', + description: 'Whether to activate FTP access (optional)' + }, + ftpPassword: { + type: 'string', + description: 'FTP password (optional, required if activateFtp is true)' + } + }, + required: ['name', 'email', 'password', 'usePublic', 'groups', 'roles'] + } + }, + { + name: 'edit_user', + description: 'Edit an existing user', + inputSchema: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Email address of the user to edit (required)' + }, + name: { + type: 'string', + description: 'New full name (optional)' + }, + password: { + type: 'string', + description: 'New password (optional)' + }, + language: { + type: 'string', + description: 'New language preference (optional)' + }, + timezone: { + type: 'string', + description: 'New timezone preference (optional)' + }, + publicUpload: { + type: 'boolean', + description: 'Whether user can upload to public (optional)' + }, + groups: { + type: 'array', + items: { + type: 'string' + }, + description: 'New groups for the user (optional)' + }, + roles: { + type: 'array', + items: { + type: 'string' + }, + description: 'New roles for the user (optional)' + }, + activateFtp: { + type: 'boolean', + description: 'Whether to activate FTP access (optional)' + }, + ftpPassword: { + type: 'string', + description: 'New FTP password (optional)' + } + }, + required: ['email'] + } + }, + { + name: 'delete_user', + description: 'Delete an existing user', + inputSchema: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Email address of the user to delete' + } + }, + required: ['email'] + } + }, + { + name: 'logout', + description: 'Logout and invalidate the current API token', + inputSchema: { + type: 'object', + properties: {} + } + } + ] + })); + + this.server.setRequestHandler(CallToolRequestSchema, async (request: Request) => { + if (!request.params?.name) { + throw new McpError(ErrorCode.InvalidRequest, 'Missing tool name'); + } + + try { + await this.ensureAuthenticated(); + console.error(`Handling tool request: ${request.params.name}`, request.params); + + switch (request.params.name) { + case 'chat': { + const args = request.params.arguments as { question: string; usePublic?: boolean; groups?: string[]; language?: string }; + console.error('Making chat request:', args); + const chatResponse = await this.axiosInstance.post('/chats', args); + console.error('Got chat response:', chatResponse.data); + return { + content: [ + { + type: 'text', + text: chatResponse.data.data.answer + } + ] + }; + } + + case 'create_source': { + const args = request.params.arguments as { name: string; content: string; groups?: string[] }; + console.error('Making create_source request:', args); + const createSourceResponse = await this.axiosInstance.post('/sources', args); + console.error('Got create_source response:', createSourceResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(createSourceResponse.data, null, 2) + } + ] + }; + } + + case 'list_groups': { + console.error('Making list_groups request'); + const listGroupsResponse = await this.axiosInstance.get('/groups'); + console.error('Got list_groups response:', listGroupsResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(listGroupsResponse.data, null, 2) + } + ] + }; + } + + case 'list_sources': { + const args = request.params.arguments as { groupName: string }; + console.error('Making list_sources request:', args); + const listSourcesResponse = await this.axiosInstance.post('/sources/groups', { + groupName: args.groupName + }); + console.error('Got list_sources response:', listSourcesResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(listSourcesResponse.data, null, 2) + } + ] + }; + } + + case 'get_source': { + const args = request.params.arguments as { sourceId: string }; + console.error('Making get_source request:', args); + const getSourceResponse = await this.axiosInstance.get(`/sources/${args.sourceId}`); + console.error('Got get_source response:', getSourceResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(getSourceResponse.data, null, 2) + } + ] + }; + } + + case 'continue_chat': { + const args = request.params.arguments as { chatId: string; question: string }; + console.error('Making continue_chat request:', args); + const continueResponse = await this.axiosInstance.patch(`/chats/${args.chatId}`, { + question: args.question + }); + console.error('Got continue_chat response:', continueResponse.data); + return { + content: [ + { + type: 'text', + text: continueResponse.data.data.answer + } + ] + }; + } + + case 'get_chat': { + const args = request.params.arguments as { chatId: string }; + console.error('Making get_chat request:', args); + const getChatResponse = await this.axiosInstance.get(`/chats/${args.chatId}`); + console.error('Got get_chat response:', getChatResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(getChatResponse.data, null, 2) + } + ] + }; + } + + case 'edit_source': { + const args = request.params.arguments as { sourceId: string; name?: string; content?: string; groups?: string[] }; + console.error('Making edit_source request:', args); + const { sourceId, ...updateData } = args; + const editResponse = await this.axiosInstance.patch(`/sources/${sourceId}`, updateData); + console.error('Got edit_source response:', editResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(editResponse.data, null, 2) + } + ] + }; + } + + case 'delete_source': { + const args = request.params.arguments as { sourceId: string }; + console.error('Making delete_source request:', args); + const deleteResponse = await this.axiosInstance.delete(`/sources/${args.sourceId}`); + console.error('Got delete_source response:', deleteResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(deleteResponse.data, null, 2) + } + ] + }; + } + + case 'create_group': { + const args = request.params.arguments as { groupName: string }; + console.error('Making create_group request:', args); + const createGroupResponse = await this.axiosInstance.post('/groups', { + groupName: args.groupName + }); + console.error('Got create_group response:', createGroupResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(createGroupResponse.data, null, 2) + } + ] + }; + } + + case 'delete_group': { + const args = request.params.arguments as { groupName: string }; + console.error('Making delete_group request:', args); + const deleteGroupResponse = await this.axiosInstance.delete('/groups', { + data: { groupName: args.groupName } + }); + console.error('Got delete_group response:', deleteGroupResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(deleteGroupResponse.data, null, 2) + } + ] + }; + } + + case 'create_user': { + const args = request.params.arguments as { + name: string; + email: string; + password: string; + language?: string; + timezone?: string; + usePublic: boolean; + groups: string[]; + roles: string[]; + activateFtp?: boolean; + ftpPassword?: string; + }; + console.error('Making create_user request:', args); + const createUserResponse = await this.axiosInstance.post('/users', args); + console.error('Got create_user response:', createUserResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(createUserResponse.data, null, 2) + } + ] + }; + } + + case 'edit_user': { + const args = request.params.arguments as { + email: string; + name?: string; + password?: string; + language?: string; + timezone?: string; + publicUpload?: boolean; + groups?: string[]; + roles?: string[]; + activateFtp?: boolean; + ftpPassword?: string; + }; + console.error('Making edit_user request:', args); + const editUserResponse = await this.axiosInstance.patch('/users', args); + console.error('Got edit_user response:', editUserResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(editUserResponse.data, null, 2) + } + ] + }; + } + + case 'delete_user': { + const args = request.params.arguments as { email: string }; + console.error('Making delete_user request:', args); + const deleteUserResponse = await this.axiosInstance.delete('/users', { + data: { email: args.email } + }); + console.error('Got delete_user response:', deleteUserResponse.data); + return { + content: [ + { + type: 'text', + text: JSON.stringify(deleteUserResponse.data, null, 2) + } + ] + }; + } + + case 'logout': { + console.error('Making logout request'); + const logoutResponse = await this.axiosInstance.delete('/logout'); + console.error('Got logout response:', logoutResponse.data); + // Clear the auth token after successful logout + this.authToken = null; + // Reset authorization header to just proxy auth (if available) + if (PROXY_USER && PROXY_PASSWORD) { + const proxyAuth = Buffer.from(`${PROXY_USER}:${PROXY_PASSWORD}`).toString('base64'); + this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${proxyAuth}`; + } else { + delete this.axiosInstance.defaults.headers.common['Authorization']; + } + return { + content: [ + { + type: 'text', + text: JSON.stringify(logoutResponse.data, null, 2) + } + ] + }; + } + + default: + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); + } + } catch (error) { + console.error('Error handling request:', error); + if (axios.isAxiosError(error)) { + const message = error.response?.data?.message ?? error.message; + console.error('API error details:', { + status: error.response?.status, + data: error.response?.data + }); + return { + content: [ + { + type: 'text', + text: `API error: ${message}` + } + ], + isError: true + }; + } + throw error; + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('PrivateGPT MCP server running on stdio'); + } +} + +const server = new PrivateGPTServer(); +server.run().catch(console.error); \ No newline at end of file diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/src/services/pgpt-service.ts b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/services/pgpt-service.ts new file mode 100644 index 0000000..b3832b7 --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/services/pgpt-service.ts @@ -0,0 +1,198 @@ +import axios, { AxiosInstance } from 'axios'; +import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; +import { ChatArgs, SourceArgs, ListSourcesArgs, GetSourceArgs } from '../types/api.js'; + +export class PGPTService { + private api: AxiosInstance; + private token: string | null = null; + private proxyAuth: string; + + constructor() { + // Set up proxy authentication + const proxyUser = 'staging@ai-testdrive.com'; + const proxyPassword = 'StagingGpt$24'; + this.proxyAuth = Buffer.from(`${proxyUser}:${proxyPassword}`).toString('base64'); + + // Initialize axios instance with base configuration + this.api = axios.create({ + baseURL: process.env.PRIVATE_GPT_API_URL || 'http://localhost:3000', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.proxyAuth}`, + }, + }); + + console.log('Getting auth token...'); + } + + private async ensureAuthenticated(): Promise { + if (!this.token) { + const email = process.env.user; + const password = process.env.password; + + if (!email || !password) { + throw new McpError( + ErrorCode.InvalidRequest, + 'Missing authentication credentials' + ); + } + + try { + const response = await this.api.post('/login', { + email, + password, + }); + this.token = response.data.data.token; + + // Combine proxy auth with bearer token + const combinedAuth = `Basic ${this.proxyAuth}, Bearer ${this.token}`; + this.api.defaults.headers.common['Authorization'] = combinedAuth; + + console.log(`Updated Authorization header: ${combinedAuth}`); + } catch (error) { + console.error('Authentication error:', error); + throw new McpError( + ErrorCode.InvalidRequest, + `Authentication failed: ${axios.isAxiosError(error) ? error.response?.data?.message || error.message : error}` + ); + } + } + } + + async chat(args: ChatArgs) { + await this.ensureAuthenticated(); + + try { + const response = await this.api.post('/chats', { + language: args.language || 'en', + question: args.question, + usePublic: args.usePublic || false, + groups: args.groups || [], + }); + + return { + content: [ + { + type: 'text', + text: response.data.data.answer, + }, + ], + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new McpError( + ErrorCode.InternalError, + `Chat failed: ${error.response?.data?.message || error.message}` + ); + } + throw error; + } + } + + async createSource(args: SourceArgs) { + await this.ensureAuthenticated(); + + try { + const response = await this.api.post('/sources', { + name: args.name, + content: args.content, + groups: args.groups || [], + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data.data, null, 2), + }, + ], + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new McpError( + ErrorCode.InternalError, + `Source creation failed: ${error.response?.data?.message || error.message}` + ); + } + throw error; + } + } + + async listGroups() { + await this.ensureAuthenticated(); + + try { + const response = await this.api.get('/groups'); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data.data, null, 2), + }, + ], + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new McpError( + ErrorCode.InternalError, + `Group listing failed: ${error.response?.data?.message || error.message}` + ); + } + throw error; + } + } + + async listSources(args: ListSourcesArgs) { + await this.ensureAuthenticated(); + + try { + const response = await this.api.post('/sources/groups', { + groupName: args.groupName, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data.data, null, 2), + }, + ], + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new McpError( + ErrorCode.InternalError, + `Source listing failed: ${error.response?.data?.message || error.message}` + ); + } + throw error; + } + } + + async getSource(args: GetSourceArgs) { + await this.ensureAuthenticated(); + + try { + const response = await this.api.get(`/sources/${args.sourceId}`); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data.data, null, 2), + }, + ], + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new McpError( + ErrorCode.InternalError, + `Source retrieval failed: ${error.response?.data?.message || error.message}` + ); + } + throw error; + } + } +} \ No newline at end of file diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/src/types/api.ts b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/types/api.ts new file mode 100644 index 0000000..5bbefeb --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/src/types/api.ts @@ -0,0 +1,242 @@ +export interface ChatArgs { + question: string; + usePublic?: boolean; + groups?: string[]; + language?: string; +} + +export interface ContinueChatArgs { + chatId: string; + question: string; +} + +export interface GetChatArgs { + chatId: string; +} + +export interface SourceArgs { + name: string; + content: string; + groups?: string[]; +} + +export interface EditSourceArgs { + sourceId: string; + name?: string; + content?: string; + groups?: string[]; +} + +export interface DeleteSourceArgs { + sourceId: string; +} + +export interface ListSourcesArgs { + groupName: string; +} + +export interface GetSourceArgs { + sourceId: string; +} + +export interface GroupArgs { + groupName: string; +} + +export interface CreateUserArgs { + name: string; + email: string; + password: string; + language?: string; + timezone?: string; + usePublic: boolean; + groups: string[]; + roles: string[]; + activateFtp?: boolean; + ftpPassword?: string; +} + +export interface EditUserArgs { + email: string; + name?: string; + password?: string; + language?: string; + timezone?: string; + publicUpload?: boolean; + groups?: string[]; + roles?: string[]; + activateFtp?: boolean; + ftpPassword?: string; +} + +export interface DeleteUserArgs { + email: string; +} + +export function validateChatArgs(args: Record | undefined): ChatArgs { + if (!args?.question || typeof args.question !== 'string') { + throw new Error('Missing or invalid question'); + } + + return { + question: args.question, + usePublic: typeof args.usePublic === 'boolean' ? args.usePublic : false, + groups: Array.isArray(args.groups) ? args.groups.map(String) : [], + language: typeof args.language === 'string' ? args.language : 'en', + }; +} + +export function validateSourceArgs(args: Record | undefined): SourceArgs { + if (!args?.name || typeof args.name !== 'string') { + throw new Error('Missing or invalid name'); + } + if (!args?.content || typeof args.content !== 'string') { + throw new Error('Missing or invalid content'); + } + + return { + name: args.name, + content: args.content, + groups: Array.isArray(args.groups) ? args.groups.map(String) : [], + }; +} + +export function validateListSourcesArgs(args: Record | undefined): ListSourcesArgs { + if (!args?.groupName || typeof args.groupName !== 'string') { + throw new Error('Missing or invalid groupName'); + } + + return { + groupName: args.groupName, + }; +} + +export function validateGetSourceArgs(args: Record | undefined): GetSourceArgs { + if (!args?.sourceId || typeof args.sourceId !== 'string') { + throw new Error('Missing or invalid sourceId'); + } + + return { + sourceId: args.sourceId, + }; +} + +export function validateContinueChatArgs(args: Record | undefined): ContinueChatArgs { + if (!args?.chatId || typeof args.chatId !== 'string') { + throw new Error('Missing or invalid chatId'); + } + if (!args?.question || typeof args.question !== 'string') { + throw new Error('Missing or invalid question'); + } + + return { + chatId: args.chatId, + question: args.question, + }; +} + +export function validateGetChatArgs(args: Record | undefined): GetChatArgs { + if (!args?.chatId || typeof args.chatId !== 'string') { + throw new Error('Missing or invalid chatId'); + } + + return { + chatId: args.chatId, + }; +} + +export function validateEditSourceArgs(args: Record | undefined): EditSourceArgs { + if (!args?.sourceId || typeof args.sourceId !== 'string') { + throw new Error('Missing or invalid sourceId'); + } + + return { + sourceId: args.sourceId, + name: typeof args.name === 'string' ? args.name : undefined, + content: typeof args.content === 'string' ? args.content : undefined, + groups: Array.isArray(args.groups) ? args.groups.map(String) : undefined, + }; +} + +export function validateDeleteSourceArgs(args: Record | undefined): DeleteSourceArgs { + if (!args?.sourceId || typeof args.sourceId !== 'string') { + throw new Error('Missing or invalid sourceId'); + } + + return { + sourceId: args.sourceId, + }; +} + +export function validateGroupArgs(args: Record | undefined): GroupArgs { + if (!args?.groupName || typeof args.groupName !== 'string') { + throw new Error('Missing or invalid groupName'); + } + + return { + groupName: args.groupName, + }; +} + +export function validateCreateUserArgs(args: Record | undefined): CreateUserArgs { + if (!args?.name || typeof args.name !== 'string') { + throw new Error('Missing or invalid name'); + } + if (!args?.email || typeof args.email !== 'string') { + throw new Error('Missing or invalid email'); + } + if (!args?.password || typeof args.password !== 'string') { + throw new Error('Missing or invalid password'); + } + if (typeof args.usePublic !== 'boolean') { + throw new Error('Missing or invalid usePublic'); + } + if (!Array.isArray(args.groups)) { + throw new Error('Missing or invalid groups'); + } + if (!Array.isArray(args.roles)) { + throw new Error('Missing or invalid roles'); + } + + return { + name: args.name, + email: args.email, + password: args.password, + language: typeof args.language === 'string' ? args.language : undefined, + timezone: typeof args.timezone === 'string' ? args.timezone : undefined, + usePublic: args.usePublic, + groups: args.groups.map(String), + roles: args.roles.map(String), + activateFtp: typeof args.activateFtp === 'boolean' ? args.activateFtp : undefined, + ftpPassword: typeof args.ftpPassword === 'string' ? args.ftpPassword : undefined, + }; +} + +export function validateEditUserArgs(args: Record | undefined): EditUserArgs { + if (!args?.email || typeof args.email !== 'string') { + throw new Error('Missing or invalid email'); + } + + return { + email: args.email, + name: typeof args.name === 'string' ? args.name : undefined, + password: typeof args.password === 'string' ? args.password : undefined, + language: typeof args.language === 'string' ? args.language : undefined, + timezone: typeof args.timezone === 'string' ? args.timezone : undefined, + publicUpload: typeof args.publicUpload === 'boolean' ? args.publicUpload : undefined, + groups: Array.isArray(args.groups) ? args.groups.map(String) : undefined, + roles: Array.isArray(args.roles) ? args.roles.map(String) : undefined, + activateFtp: typeof args.activateFtp === 'boolean' ? args.activateFtp : undefined, + ftpPassword: typeof args.ftpPassword === 'string' ? args.ftpPassword : undefined, + }; +} + +export function validateDeleteUserArgs(args: Record | undefined): DeleteUserArgs { + if (!args?.email || typeof args.email !== 'string') { + throw new Error('Missing or invalid email'); + } + + return { + email: args.email, + }; +} \ No newline at end of file diff --git a/clients/Gradio/mcp_servers/pgpt-mcp-server/tsconfig.json b/clients/Gradio/mcp_servers/pgpt-mcp-server/tsconfig.json new file mode 100644 index 0000000..26e8a66 --- /dev/null +++ b/clients/Gradio/mcp_servers/pgpt-mcp-server/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@modelcontextprotocol/sdk": ["node_modules/@modelcontextprotocol/sdk"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests", "**/*.test.ts"] +} \ No newline at end of file