├── assets
├── mcp-logo-wide.png
└── mcp-settings.css
├── .gitignore
├── src
└── Convo
│ └── Gpt
│ ├── ContextLengthExceededException.php
│ ├── RefuseFunctionCallException.php
│ ├── Pckg
│ ├── GptPackageDefinition.json
│ ├── Help
│ │ ├── actions-prompt-element.html
│ │ ├── conversation-messages-element.md
│ │ ├── embeddings-element.md
│ │ ├── chat-completion-element.md
│ │ ├── group-system-messages-element.md
│ │ ├── system-message-element.md
│ │ ├── moderation-api-element.md
│ │ └── simple-message-limiter-element.md
│ ├── ConversationMessagesElement.php
│ ├── SystemMessageElement.php
│ ├── ModerationApiElement.php
│ ├── MessagesLimiterElement.php
│ ├── EmbeddingsElement.php
│ ├── SimpleMcpPromptTemplate.php
│ ├── ChatCompletionElement.php
│ ├── mcp-server-project.template.json
│ ├── SystemMessageGroupElement.php
│ ├── ExternalChatFunctionElement.php
│ ├── ChatFunctionElement.php
│ ├── SimpleMessagesLimiterElement.php
│ └── WpRestProxyFunction.php
│ ├── IMessages.php
│ ├── IChatFunctionContainer.php
│ ├── GptApiFactory.php
│ ├── IChatFunction.php
│ ├── Tools
│ ├── AbstractRestFunctions.php
│ ├── SettingsRestFunctions.php
│ ├── TaxonomyRestFunctions.php
│ ├── PluginRestFunctions.php
│ ├── MediaRestFunctions.php
│ ├── PagesRestFunctions.php
│ ├── CommentRestFunctions.php
│ ├── UserRestFunctions.php
│ └── PostRestFunctions.php
│ ├── Mcp
│ ├── McpSessionManagerFactory.php
│ ├── McpServerPublisher.php
│ ├── StreamWriter.php
│ ├── SseResponse.php
│ ├── McpServerPlatform.php
│ ├── IMcpSessionStoreInterface.php
│ ├── StreamHandler.php
│ ├── McpSessionManager.php
│ ├── McpServerCommandRequest.php
│ ├── McpFilesystemSessionStore.php
│ └── CommandDispatcher.php
│ ├── FunctionResultTooLargeException.php
│ ├── GptPlugin.php
│ ├── Admin
│ ├── SettingsViewModel.php
│ ├── SettingsProcessor.php
│ └── McpConvoworksManager.php
│ ├── GptApi.php
│ └── PluginContext.php
├── phpunit.xml
├── update.json
├── package.json
├── .editorconfig
├── .vscode
└── settings.json
├── phpstan.neon
├── composer.json
├── sync-version.js
├── phpstan-bootstrap.php
├── phpstan-stubs
└── convoworks-wp.php
├── .github
└── workflows
│ ├── release.yml
│ └── update-json.yml
├── tests
├── SummarizeMessagesTest.php
└── ProcessJsonWithConstantsTest.php
├── convoworks-gpt.php
├── .phpstan-setup.md
├── .cursorrules
└── CHANGELOG.md
/assets/mcp-logo-wide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zef-dev/convoworks-gpt/HEAD/assets/mcp-logo-wide.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /dist/
3 | /composer.lock
4 | /.phpunit.result.cache
5 | /.workspace/
6 | /node_modules/
7 | /package-lock.json
8 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/ContextLengthExceededException.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "convoworks-gpt/convoworks-gpt.php": {
3 | "version": "0.16.1",
4 | "package": "https://github.com/zef-dev/convoworks-gpt/releases/download/v0.16.1/convoworks-gpt-v0.16.1.zip",
5 | "requires": "5.0",
6 | "tested": "6.6",
7 | "last_updated": "2025-12-04"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/GptPackageDefinition.json:
--------------------------------------------------------------------------------
1 | {
2 | "namespace": "convo-gpt",
3 | "author": "ZEF Development",
4 | "email": "tihomir@convoworks.com",
5 | "source_for": [
6 | "templates", "elements"
7 | ],
8 | "description": "Contains components for GPT",
9 | "stability": "stable"
10 | }
11 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/actions-prompt-element.html:
--------------------------------------------------------------------------------
1 |
2 |
Actions Prompt Generator
3 |
4 |
5 | This element will collect all defined actions and insert their prompts.
6 |
7 |
8 |
9 | You can use a local prompt title and content to improve specification intro.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "version": "0.16.1",
4 | "scripts": {
5 | "build" : "node sync-version.js && node build.js",
6 | "sync-version": "node sync-version.js"
7 | },
8 | "devDependencies": {
9 | "archiver": "^5.3.2",
10 | "fs-extra": "^11.2.0",
11 | "yargs": "^17.0.1"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/IMessages.php:
--------------------------------------------------------------------------------
1 | =7.2.5"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "Convo\\Gpt\\": "src/Convo/Gpt"
19 | }
20 | },
21 | "require-dev": {
22 | "phpunit/phpunit": "^8",
23 | "zef-dev/convoworks-core": "^0.22",
24 | "phpstan/phpstan": "*",
25 | "php-stubs/wordpress-stubs": "^6.9"
26 | },
27 | "autoload-dev": {
28 | "psr-4": {
29 | "Convo\\": "tests/"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/GptApiFactory.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
26 | $this->_httpFactory = $httpFactory;
27 | }
28 |
29 | public function getApi($apiKey, $baseUrl = null)
30 | {
31 | return new GptApi($this->_logger, $this->_httpFactory, $apiKey, $baseUrl);
32 | }
33 |
34 |
35 | // UTIL
36 | public function __toString()
37 | {
38 | return get_class($this) . '[]';
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/IChatFunction.php:
--------------------------------------------------------------------------------
1 | $val) {
17 | $request->set_param($key, $val);
18 | }
19 | } else {
20 | $request->set_body_params($data);
21 | }
22 | $response = rest_do_request($request);
23 | return json_encode($response->get_data());
24 | };
25 | }
26 |
27 |
28 | // UTIL
29 | public function __toString()
30 | {
31 | return get_class($this) . '[]';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sync-version.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | // Read the version from package.json
4 | const packageJson = require('./package.json');
5 | const version = packageJson.version;
6 |
7 | // Update composer.json
8 | const composerJsonPath = './composer.json';
9 | const composerJson = require(composerJsonPath);
10 | composerJson.version = version;
11 | fs.writeFileSync(composerJsonPath, JSON.stringify(composerJson, null, 4), 'utf8');
12 |
13 | // Update PHP file where the version is defined
14 | const phpFilePath = './convoworks-gpt.php';
15 | let phpContent = fs.readFileSync(phpFilePath, 'utf8');
16 |
17 | // Replace the version in the define() statement
18 | phpContent = phpContent.replace(/define\( 'CONVO_GPT_VERSION', '.*' \);/, `define( 'CONVO_GPT_VERSION', '${version}' );`);
19 |
20 | // Replace the version in the plugin header comment
21 | phpContent = phpContent.replace(/(\* Version:\s*)[0-9\.]+/, `$1${version}`);
22 |
23 | fs.writeFileSync(phpFilePath, phpContent, 'utf8');
24 |
25 | console.log(`Version synchronized to ${version}`);
26 |
--------------------------------------------------------------------------------
/assets/mcp-settings.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Convoworks MCP Server Settings Page Styles
3 | */
4 |
5 | .settings-label {
6 | vertical-align: top;
7 | font-weight: bold;
8 | min-width: 100px;
9 | max-width: 200px;
10 | }
11 |
12 | .settings-input input.setting-text {
13 | width: 100%;
14 | }
15 |
16 | #special_role {
17 | width: 200px;
18 | display: block;
19 | margin-bottom: 10px;
20 | }
21 |
22 | #wrapbox {
23 | margin-right: 25px;
24 | }
25 |
26 | .settings-spacer {
27 | height: 7px;
28 | }
29 |
30 | .form-invalid.form-required textarea {
31 | border-color: #d63638 !important;
32 | box-shadow: 0 0 2px rgba(214, 54, 56, .8);
33 | }
34 |
35 | .settings-row .settings-input textarea,
36 | .settings-row .settings-input select {
37 | width: 100%;
38 | max-width: 100%;
39 | }
40 |
41 | .settings-helper {
42 | vertical-align: top;
43 | font-style: italic;
44 | min-width: 200px;
45 | max-width: 500px;
46 | line-height: 16px;
47 | }
48 |
49 | .settings-input {
50 | vertical-align: top;
51 | min-width: 100px;
52 | max-width: 300px;
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/phpstan-bootstrap.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
27 | $this->_basePath = $basePath;
28 | }
29 |
30 | public function getSessionManager($serviceId): McpSessionManager
31 | {
32 | if (!isset($this->_instances[$serviceId])) {
33 | $this->_logger->debug("Creating new McpSessionManager for service: $serviceId");
34 | $mcp_store = new McpFilesystemSessionStore($this->_logger, $this->_basePath, $serviceId);
35 | $this->_instances[$serviceId] = new McpSessionManager(
36 | $this->_logger,
37 | $mcp_store
38 | );
39 | }
40 | return $this->_instances[$serviceId];
41 | }
42 |
43 | // UTIL
44 | public function __toString()
45 | {
46 | return get_class($this) . '[]';
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/McpServerPublisher.php:
--------------------------------------------------------------------------------
1 | _checkEnabled();
32 |
33 | $this->_serviceReleaseManager->initDevelopmentRelease($this->_user, $this->_serviceId, $this->getPlatformId(), 'a');
34 | }
35 |
36 | public function delete(array &$report)
37 | {
38 | $this->_serviceReleaseManager->withdrawPlatform($this->_user, $this->_serviceId, McpServerPlatform::PLATFORM_ID);
39 | }
40 |
41 | public function getStatus()
42 | {
43 | return ['status' => IPlatformPublisher::SERVICE_PROPAGATION_STATUS_FINISHED];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/ConversationMessagesElement.php:
--------------------------------------------------------------------------------
1 | _messages = $properties['messages'];
22 | }
23 |
24 | public function read(IConvoRequest $request, IConvoResponse $response)
25 | {
26 | /** @var \Convo\Gpt\IMessages $container */
27 | $container = $this->findAncestor('\Convo\Gpt\IMessages');
28 |
29 | $messages = $this->evaluateString($this->_messages);
30 |
31 | if (is_array($messages)) {
32 | $this->_logger->debug('Got loaded messages [' . count($messages) . ']');
33 | foreach ($messages as $message) {
34 | $container->registerMessage($message);
35 | }
36 | }
37 | }
38 |
39 | // UTIL
40 | public function __toString()
41 | {
42 | return parent::__toString() . '[' . $this->_messages . ']';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/SettingsRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'get_settings',
24 | 'description' => 'Retrieve WordPress settings.',
25 | 'parameters' => [],
26 | 'defaults' => [],
27 | 'required' => [],
28 | 'execute' => $this->_makeRestFn('GET', 'settings')
29 | ],
30 | [
31 | 'name' => 'update_settings',
32 | 'description' => 'Update WordPress settings.',
33 | 'parameters' => [
34 | 'settings' => ['type' => 'array', 'description' => 'Settings key-value pairs']
35 | ],
36 | 'defaults' => [],
37 | 'required' => ['settings'],
38 | 'execute' => function ($data) {
39 | return $this->_makeRestFn('POST', 'settings')($data['settings']);
40 | }
41 | ]
42 | ];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/TaxonomyRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_taxonomies',
24 | 'description' => 'List WordPress taxonomies.',
25 | 'parameters' => [],
26 | 'defaults' => [],
27 | 'required' => [],
28 | 'execute' => $this->_makeRestFn('GET', 'taxonomies')
29 | ],
30 | [
31 | 'name' => 'get_taxonomy',
32 | 'description' => 'Retrieve a taxonomy by slug.',
33 | 'parameters' => [
34 | 'slug' => ['type' => 'string', 'description' => 'Taxonomy slug']
35 | ],
36 | 'defaults' => [],
37 | 'required' => ['slug'],
38 | 'execute' => function ($data) {
39 | $fn = $this->_makeRestFn('GET', "taxonomies/{$data['slug']}");
40 | return $fn($data);
41 | }
42 | ]
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/phpstan-stubs/convoworks-wp.php:
--------------------------------------------------------------------------------
1 | _content = $properties['content'];
23 | $this->_disableEval = $properties['disable_eval'] ?? '';
24 | }
25 |
26 | public function read(IConvoRequest $request, IConvoResponse $response)
27 | {
28 | /** @var \Convo\Gpt\IMessages $container */
29 | $container = $this->findAncestor('\Convo\Gpt\IMessages');
30 |
31 | $disabled = boolval($this->evaluateString($this->_disableEval));
32 |
33 | if ($disabled) {
34 | $container->registerMessage([
35 | 'role' => 'system',
36 | 'transient' => true,
37 | 'content' => $this->_content
38 | ]);
39 | } else {
40 | $container->registerMessage([
41 | 'role' => 'system',
42 | 'transient' => true,
43 | 'content' => $this->evaluateString($this->_content)
44 | ]);
45 | }
46 | }
47 |
48 | // UTIL
49 | public function __toString()
50 | {
51 | return parent::__toString() . '[' . $this->_content . ']';
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*" # Triggers the workflow when pushing a tag that starts with 'v'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: "14"
20 |
21 | - name: Install Node.js dependencies
22 | run: npm install
23 |
24 | - name: Sync version across files
25 | run: npm run sync-version
26 |
27 | - name: Set up PHP
28 | uses: shivammathur/setup-php@v2
29 | with:
30 | php-version: "7.4"
31 | tools: composer
32 |
33 | - name: Install PHP dependencies
34 | run: composer install --no-dev --prefer-dist
35 |
36 | - name: Run build script
37 | run: npm run build
38 |
39 | - name: Debug - List files in build directory
40 | run: ls -al ./build
41 |
42 | - name: Debug - Check file existence
43 | run: |
44 | if [ ! -f "./build/convoworks-gpt-${{ github.ref_name }}.zip" ]; then
45 | echo "Error: File not found!"
46 | exit 1
47 | fi
48 |
49 | - name: Create GitHub Release
50 | uses: softprops/action-gh-release@v1
51 | with:
52 | files: ./build/convoworks-gpt-${{ github.ref_name }}.zip
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} # Use PAT_TOKEN instead of GITHUB_TOKEN
55 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/StreamWriter.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
23 | }
24 |
25 | /**
26 | * Sends HTTP headers for the stream.
27 | *
28 | * @param array $headers Associative array of header name => value.
29 | */
30 | public function sendHeaders(array $headers): void
31 | {
32 | foreach ($headers as $name => $value) {
33 | header("$name: $value");
34 | }
35 | flush();
36 | $this->_logger->debug('Sent headers: ' . json_encode($headers));
37 | }
38 |
39 | /**
40 | * Sends an SSE event.
41 | *
42 | * @param string $event Event name (e.g., 'message').
43 | * @param string $data JSON-encoded data.
44 | */
45 | public function sendEvent(string $event, string $data): void
46 | {
47 | echo "event: $event\n";
48 | echo "data: $data\n\n";
49 | flush();
50 | $this->_logger->debug("Sent SSE event [$event]: $data");
51 | }
52 |
53 | /**
54 | * Sends a raw message line for bidirectional streaming.
55 | *
56 | * @param string $json JSON-encoded message.
57 | */
58 | public function sendMessage(string $json): void
59 | {
60 | echo $json . "\n";
61 | flush();
62 | $this->_logger->debug("Sent message: $json");
63 | }
64 |
65 | /**
66 | * Sends a ping message.
67 | */
68 | public function sendPing(): void
69 | {
70 | $ping = json_encode(['jsonrpc' => '2.0', 'method' => 'ping']);
71 | $this->sendMessage($ping);
72 | $this->_logger->debug('Sent ping');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/PluginRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_plugins',
24 | 'description' => 'List installed WordPress plugins.',
25 | 'parameters' => [],
26 | 'defaults' => [],
27 | 'required' => [],
28 | 'execute' => $this->_makeRestFn('GET', 'plugins')
29 | ],
30 | [
31 | 'name' => 'activate_plugin',
32 | 'description' => 'Activate a plugin by slug.',
33 | 'parameters' => [
34 | 'slug' => ['type' => 'string', 'description' => 'Plugin slug']
35 | ],
36 | 'defaults' => [],
37 | 'required' => ['slug'],
38 | 'execute' => function ($data) {
39 | return $this->_makeRestFn('POST', "plugins/{$data['slug']}/activate")([]);
40 | }
41 | ],
42 | [
43 | 'name' => 'deactivate_plugin',
44 | 'description' => 'Deactivate a plugin by slug.',
45 | 'parameters' => [
46 | 'slug' => ['type' => 'string', 'description' => 'Plugin slug']
47 | ],
48 | 'defaults' => [],
49 | 'required' => ['slug'],
50 | 'execute' => function ($data) {
51 | return $this->_makeRestFn('POST', "plugins/{$data['slug']}/deactivate")([]);
52 | }
53 | ]
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/FunctionResultTooLargeException.php:
--------------------------------------------------------------------------------
1 | _result = $result;
15 | }
16 |
17 | /**
18 | * Returns structured response including error message and result info:
19 | * - If JSON: includes 'structure' key with scanned structure
20 | * - Otherwise: includes 'preview' key with first 250 chars
21 | *
22 | * @return array
23 | */
24 | public function getResponse()
25 | {
26 | if (!$this->_responseGenerated) {
27 | $this->_generateResponse();
28 | }
29 |
30 | $response = [
31 | 'error' => $this->getMessage()
32 | ];
33 |
34 | if ($this->_structure !== null) {
35 | $response['structure'] = $this->_structure;
36 | } else {
37 | $response['preview'] = substr($this->_result, 0, 250);
38 | }
39 |
40 | return $response;
41 | }
42 |
43 | private function _generateResponse()
44 | {
45 | $this->_responseGenerated = true;
46 |
47 | // Try to parse as JSON
48 | $result_data = json_decode($this->_result, true);
49 |
50 | if (json_last_error() === JSON_ERROR_NONE) {
51 | // Successfully parsed as JSON - scan structure
52 | $this->_structure = Util::scanStructure($result_data);
53 | }
54 | // Otherwise structure stays null and getResponse() returns substring
55 | }
56 |
57 | /**
58 | * @deprecated Use getResponse() instead
59 | */
60 | public function getStructure() : ?array
61 | {
62 | if (!$this->_responseGenerated) {
63 | $this->_generateResponse();
64 | }
65 | return $this->_structure;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/SseResponse.php:
--------------------------------------------------------------------------------
1 | _sessionId = $sessionId;
25 | $this->_logger = new NullLogger();
26 | }
27 |
28 |
29 | public function setPlatformResponse($data)
30 | {
31 | $this->_platformResponse = $data;
32 | }
33 |
34 | public function getPlatformResponse()
35 | {
36 | return is_array($this->_platformResponse) && empty($this->_platformResponse)
37 | ? new \stdClass()
38 | : $this->_platformResponse;
39 | }
40 |
41 |
42 | public function addText($text, $append = false): void {}
43 |
44 |
45 | public function getText() {}
46 |
47 |
48 | public function setShouldEndSession($shouldEndSession)
49 | {
50 | throw new \RuntimeException('Not implemented');
51 | }
52 |
53 | public function shouldEndSession(): bool
54 | {
55 | throw new \RuntimeException('Not implemented');
56 | }
57 |
58 | public function isEmpty(): bool
59 | {
60 | throw new \RuntimeException('Not implemented');
61 | }
62 |
63 | public function isSsml(): bool
64 | {
65 | throw new \RuntimeException('Not implemented');
66 | }
67 |
68 | public function addRepromptText($text, $append = false) {}
69 |
70 | public function getRepromptText() {}
71 |
72 | public function getTextSsml()
73 | {
74 | throw new \RuntimeException('Not implemented');
75 | }
76 |
77 | public function getRepromptTextSsml()
78 | {
79 | throw new \RuntimeException('Not implemented');
80 | }
81 |
82 |
83 | public function setLogger(LoggerInterface $logger)
84 | {
85 | $this->_logger = $logger;
86 | }
87 |
88 | // UTIL
89 | public function __toString()
90 | {
91 | return get_class($this) . '[' . $this->_sessionId . ']';
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/McpServerPlatform.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
47 | $this->_publicHandler = $publicHandler;
48 | $this->_convoServiceDataProvider = $serviceDataProvider;
49 | $this->_serviceReleaseManager = $serviceReleaseManager;
50 | }
51 |
52 | public function getPlatformId()
53 | {
54 | return self::PLATFORM_ID;
55 | }
56 |
57 | public function getPublicRestHandler()
58 | {
59 | return $this->_publicHandler;
60 | }
61 |
62 | public function getPlatformPublisher(IAdminUser $user, $serviceId)
63 | {
64 | if (!isset($this->_platformPublisher)) {
65 | $this->_platformPublisher = new McpServerPublisher(
66 | $this->_logger,
67 | $user,
68 | $serviceId,
69 | $this->_convoServiceDataProvider,
70 | $this->_serviceReleaseManager
71 | );
72 | }
73 |
74 | return $this->_platformPublisher;
75 | }
76 |
77 | // UTIL
78 | public function __toString()
79 | {
80 | return get_class($this) . '[' . $this->getPlatformId() . ']';
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/ModerationApiElement.php:
--------------------------------------------------------------------------------
1 | _gptApiFactory = $gptApiFactory;
32 |
33 | foreach ($properties['ok'] as $element) {
34 | $this->_ok[] = $element;
35 | $this->addChild($element);
36 | }
37 | }
38 |
39 | public function read(IConvoRequest $request, IConvoResponse $response)
40 | {
41 | $input = $this->evaluateString($this->_properties['input']);
42 | $api_key = $this->evaluateString($this->_properties['api_key']);
43 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null);
44 |
45 | $api = $this->_gptApiFactory->getApi($api_key, $base_url);
46 | $http_response = $api->moderations($this->_buildApiOptions($input));
47 |
48 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this);
49 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response);
50 |
51 | foreach ($this->_ok as $elem) {
52 | $elem->read($request, $response);
53 | }
54 | }
55 |
56 | private function _buildApiOptions($input)
57 | {
58 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this);
59 | $options['input'] = $input;
60 | return $options;
61 | }
62 |
63 | // UTIL
64 | public function __toString()
65 | {
66 | return parent::__toString() . '[]';
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/MessagesLimiterElement.php:
--------------------------------------------------------------------------------
1 | _gptApiFactory = $gptApiFactory;
22 | }
23 |
24 | protected function _truncateMessages($allMessages)
25 | {
26 | $messages = parent::_truncateMessages($allMessages);
27 | $truncated = Util::getTruncatedPart($allMessages, $messages);
28 |
29 | if ($truncated) {
30 | $this->_logger->debug('Truncated messages [' . print_r($truncated, true) . ']');
31 | $summarized = $this->_sumarize($truncated);
32 | $messages = array_merge([['role' => 'system', 'content' => $summarized]], $messages);
33 | }
34 | return $messages;
35 | }
36 |
37 | private function _sumarize($conversation)
38 | {
39 | $messages = [[
40 | 'role' => 'system',
41 | 'content' => $this->evaluateString($this->_properties['system_message'])
42 | ]];
43 |
44 | $messages[] = [
45 | 'role' => 'user',
46 | 'content' => Util::serializeGptMessages($conversation, true)
47 | ];
48 |
49 | $api_key = $this->evaluateString($this->_properties['api_key']);
50 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null);
51 |
52 | $api = $this->_gptApiFactory->getApi($api_key, $base_url);
53 |
54 | $http_response = $api->chatCompletion($this->_buildApiOptions($messages));
55 |
56 | return $http_response['choices'][0]['message']['content'];
57 | }
58 |
59 | private function _buildApiOptions($messages)
60 | {
61 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this);
62 |
63 | $options['messages'] = $messages;
64 |
65 | return $options;
66 | }
67 |
68 | // UTIL
69 | public function __toString()
70 | {
71 | return parent::__toString() . '[]';
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/.github/workflows/update-json.yml:
--------------------------------------------------------------------------------
1 | name: Update Plugin JSON and Sync Versions
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | update-json-and-sync-versions:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout main branch
13 | uses: actions/checkout@v3
14 | with:
15 | ref: main # Explicitly check out the main branch
16 |
17 | - name: Set up Node.js
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: "14"
21 |
22 | - name: Set up PHP
23 | uses: shivammathur/setup-php@v2
24 | with:
25 | php-version: "7.4"
26 |
27 | - name: Install dependencies
28 | run: |
29 | npm install
30 | composer install --no-dev --prefer-dist
31 |
32 | - name: Sync version in files
33 | run: npm run sync-version # Assuming you have a script that syncs versions
34 |
35 | - name: Update JSON file
36 | run: |
37 | version="${{ github.event.release.tag_name }}"
38 | version="${version#v}"
39 |
40 | download_url="https://github.com/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}/convoworks-gpt-v${version}.zip"
41 | updated_date=$(date -u +"%Y-%m-%d")
42 |
43 | cat > update.json <_gptApiFactory = $gptApiFactory;
32 |
33 | foreach ($properties['ok'] as $element) {
34 | $this->_ok[] = $element;
35 | $this->addChild($element);
36 | }
37 | }
38 |
39 | public function read(IConvoRequest $request, IConvoResponse $response)
40 | {
41 | $input = $this->evaluateString($this->_properties['input']);
42 |
43 | $api_key = $this->evaluateString($this->_properties['api_key']);
44 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null);
45 | $api = $this->_gptApiFactory->getApi($api_key, $base_url);
46 |
47 | $this->_logger->debug('Got input ============');
48 | $this->_logger->debug("\n" . print_r($input, true));
49 | $this->_logger->debug('============');
50 |
51 | $http_response = $api->embeddings($this->_buildApiOptions($input));
52 |
53 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this);
54 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response);
55 |
56 | foreach ($this->_ok as $elem) {
57 | $elem->read($request, $response);
58 | }
59 | }
60 |
61 | private function _buildApiOptions($input)
62 | {
63 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this);
64 | $options['input'] = $input;
65 | return $options;
66 | }
67 |
68 | // UTIL
69 | public function __toString()
70 | {
71 | return parent::__toString() . '[]';
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/SummarizeMessagesTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $result, "Failed asserting that serialized messages are as expected.");
16 | }
17 |
18 | public function shouldHandleWHenContentIsNull()
19 | {
20 | return [
21 | [
22 | 'messages' => [
23 | [
24 | 'role' => 'assistant',
25 | 'content' => null,
26 | 'tool_calls' => [
27 | [
28 | 'id' => 'call_glNresze0Hjxix79HKUOKTPi',
29 | 'type' => 'function',
30 | 'function' => [
31 | 'name' => 'email_verify',
32 | 'arguments' => '{"email":"tole@zefdev.com","code":7921}',
33 | ],
34 | ],
35 | ],
36 | 'refusal' => '',
37 | ],
38 | [
39 | 'role' => 'tool',
40 | 'tool_call_id' => 'call_glNresze0Hjxix79HKUOKTPi',
41 | 'content' => '{"verified" : true}',
42 | ],
43 | [
44 | 'role' => 'assistant',
45 | 'content' => 'Your email tole@zefdev.com has been successfully verified. Now, let\'s schedule your demo. Could you please tell me your preferred date and time for the demo? Remember, demos can be scheduled between 9:00 AM and 4:30 PM during the workweek, and we need to avoid same-day or next-day bookings.',
46 | 'refusal' => '',
47 | ],
48 | ],
49 | 'expected' => "Assistant: Your email tole@zefdev.com has been successfully verified. Now, let's schedule your demo. Could you please tell me your preferred date and time for the demo? Remember, demos can be scheduled between 9:00 AM and 4:30 PM during the workweek, and we need to avoid same-day or next-day bookings.",
50 | ],
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/SimpleMcpPromptTemplate.php:
--------------------------------------------------------------------------------
1 | _name = $properties['name'];
26 | $this->_description = $properties['description'];
27 | $this->_prompt = $properties['prompt'];
28 | $this->_arguments = $properties['arguments'];
29 | }
30 |
31 | public function read(IConvoRequest $request, IConvoResponse $response)
32 | {
33 | try {
34 | /** @var McpServerProcessor $container */
35 | $container = $this->findAncestor('\Convo\Gpt\Pckg\McpServerProcessor');
36 | $container->registerPrompt([
37 | 'name' => $this->evaluateString($this->_name),
38 | 'description' => $this->evaluateString($this->_description),
39 | 'arguments' => $this->getService()->evaluateArgs($this->_arguments, $this),
40 | 'template' => $this->_prompt
41 | ]);
42 | } catch (DataItemNotFoundException $e) {
43 | $this->_logger->warning('Failed to find ancestor McpServerProcessor: ' . $e->getMessage());
44 | }
45 |
46 | // act as reguklar system message
47 | try {
48 | /** @var \Convo\Gpt\IMessages $container */
49 | $container = $this->findAncestor('\Convo\Gpt\IMessages');
50 |
51 | $container->registerMessage([
52 | 'role' => 'system',
53 | 'transient' => true,
54 | 'content' => $this->evaluateString($this->_prompt)
55 | ]);
56 | } catch (DataItemNotFoundException $e) {
57 | $this->_logger->warning('Failed to find ancestor IMessages: ' . $e->getMessage());
58 | }
59 | }
60 |
61 | // UTIL
62 | public function __toString()
63 | {
64 | return parent::__toString() . '[' . $this->_name . '][' . $this->_description . ']';
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/IMcpSessionStoreInterface.php:
--------------------------------------------------------------------------------
1 | ..., 'data' => ...]
58 | * @return void
59 | */
60 | public function queueEvent(string $sessionId, array $data): void;
61 |
62 | /**
63 | * Persists multiple events/messages for the session.
64 | *
65 | * @param string $sessionId
66 | * @param array $events
67 | * @return void
68 | */
69 | public function queueEvents(string $sessionId, array $events): void;
70 |
71 | /**
72 | * Retrieves and removes the next queued message for the session.
73 | *
74 | * @param string $sessionId
75 | * @return array|null
76 | */
77 | public function nextEvent(string $sessionId): ?array;
78 |
79 | /**
80 | * Deletes session files and folders that have been inactive for the given time (in seconds).
81 | *
82 | * @param int $inactiveTime Seconds of inactivity before deletion.
83 | * @return int Number of deleted sessions.
84 | */
85 | public function cleanupInactiveSessions(int $inactiveTime): int;
86 | }
87 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/ChatCompletionElement.php:
--------------------------------------------------------------------------------
1 | _gptApiFactory = $gptApiFactory;
32 |
33 | foreach ($properties['ok'] as $element) {
34 | $this->_ok[] = $element;
35 | $this->addChild($element);
36 | }
37 | }
38 |
39 | public function read(IConvoRequest $request, IConvoResponse $response)
40 | {
41 | $system_message = $this->evaluateString($this->_properties['system_message']);
42 | $messages = $this->evaluateString($this->_properties['messages']);
43 |
44 | $messages = array_merge(
45 | [['role' => 'system', 'content' => $system_message]],
46 | $messages
47 | );
48 |
49 | $api_key = $this->evaluateString($this->_properties['api_key']);
50 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null);
51 | $api = $this->_gptApiFactory->getApi($api_key, $base_url);
52 |
53 | $this->_logger->debug('Got messages ============');
54 | $this->_logger->debug("\n" . json_encode($messages, JSON_PRETTY_PRINT));
55 | $this->_logger->debug('============');
56 |
57 | $http_response = $api->chatCompletion($this->_buildApiOptions($messages));
58 |
59 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this);
60 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response);
61 |
62 | foreach ($this->_ok as $elem) {
63 | $elem->read($request, $response);
64 | }
65 | }
66 |
67 | private function _buildApiOptions($messages)
68 | {
69 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this);
70 | $options['messages'] = $messages;
71 | return $options;
72 | }
73 |
74 | // UTIL
75 | public function __toString()
76 | {
77 | return parent::__toString() . '[]';
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/mcp-server-project.template.json:
--------------------------------------------------------------------------------
1 | {
2 | "template_id": "mcp-server-project",
3 | "name": "MCP Server Project",
4 | "description": "Blank, MCP server starter template.",
5 | "service": {
6 | "convo_service_version": 40,
7 | "packages": [
8 | "convo-core",
9 | "convo-gpt"
10 | ],
11 | "contexts": [],
12 | "variables": [],
13 | "preview_variables": [],
14 | "entities": [],
15 | "intents": [],
16 | "blocks": [
17 | {
18 | "class": "\\Convo\\Pckg\\Core\\Elements\\SpecialRoleProcessorBlock",
19 | "namespace": "convo-core",
20 | "properties": {
21 | "block_id": "MCP_Server",
22 | "name": "MCP Server",
23 | "role": "mcp-server",
24 | "processors": [
25 | {
26 | "class": "\\Convo\\Gpt\\Pckg\\McpServerProcessor",
27 | "namespace": "convo-gpt",
28 | "properties": {
29 | "name": "WP MCP Server",
30 | "version": "1.0",
31 | "tools": [],
32 | "_component_id": "6p0uigl8-lbqp-8if5-diuw-xwwvijqgfbtf"
33 | }
34 | }
35 | ],
36 | "failback": [],
37 | "_component_id": "b9th0mg2-ymeo-hhkt-orif-ozjcafiqs2a0"
38 | }
39 | },
40 | {
41 | "class": "\\Convo\\Pckg\\Core\\Elements\\ConversationBlock",
42 | "namespace": "convo-core",
43 | "properties": {
44 | "role": "error_handler",
45 | "block_id": "Error_handler",
46 | "name": "Error handler",
47 | "pre_dispatch": [],
48 | "elements": [
49 | {
50 | "class": "\\Convo\\Pckg\\Core\\Elements\\LogElement",
51 | "namespace": "convo-core",
52 | "properties": {
53 | "log_message": "${error.getMessage()}",
54 | "_component_id": "pqamtg9p-fdnj-tvdq-1ckm-3ynp5zznvg2n"
55 | }
56 | }
57 | ],
58 | "processors": [],
59 | "fallback": [],
60 | "_component_id": "701jr3fq-gior-sdop-w9or-cdgglgnvp1k8"
61 | }
62 | }
63 | ],
64 | "fragments": [],
65 | "properties": [],
66 | "configurations": []
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/SystemMessageGroupElement.php:
--------------------------------------------------------------------------------
1 | _trimChildren = $properties['trim_children'];
28 | foreach ( $properties['message_provider'] as $element) {
29 | $this->_messagesDefinition[] = $element;
30 | $this->addChild($element);
31 | }
32 | }
33 |
34 | public function read( IConvoRequest $request, IConvoResponse $response)
35 | {
36 |
37 | // collect inner messages first
38 | $this->_messages = [];
39 |
40 | foreach ( $this->_messagesDefinition as $elem) {
41 | $elem->read( $request, $response);
42 | }
43 |
44 | /** @var \Convo\Gpt\IMessages $container */
45 | $container = $this->findAncestor( '\Convo\Gpt\IMessages');
46 |
47 | $container->registerMessage( $this->_getCompleteMessage());
48 | }
49 |
50 | /**
51 | * @param array $message
52 | */
53 | public function registerMessage( $message) {
54 | $this->_messages[] = $message;
55 | }
56 |
57 | /**
58 | * Returns all messages
59 | * @return array
60 | */
61 | public function getMessages() {
62 | return [$this->_getCompleteMessage()];
63 | }
64 |
65 | private function _getCompleteMessage() {
66 | return [
67 | 'role' => 'system',
68 | 'transient' => true,
69 | 'content' => $this->_getCompleteContent()
70 | ];
71 | }
72 |
73 |
74 | private function _getCompleteContent() {
75 | $content = '';
76 | $trim = $this->evaluateString( $this->_trimChildren);
77 | if ( $trim) {
78 | foreach ( $this->_messages as $message) {
79 | $content .= trim( $message['content']);
80 | }
81 | } else {
82 | foreach ( $this->_messages as $message) {
83 | $content .= "\n\n".$message['content'];
84 | }
85 | }
86 |
87 | return $content;
88 | }
89 |
90 |
91 | // UTIL
92 | public function __toString()
93 | {
94 | return parent::__toString().'['.$this->_trimChildren.']['.count( $this->_messagesDefinition).']';
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/convoworks-gpt.php:
--------------------------------------------------------------------------------
1 | register();
54 | }
55 | run_convoworks_gpt_plugin();
56 |
57 | function convoworks_gpt_check_for_updates($update, $plugin_data, $plugin_file)
58 | {
59 | static $response = false;
60 |
61 | if (empty($plugin_data['UpdateURI']) || !empty($update)) {
62 | return $update;
63 | }
64 |
65 | if ($response === false) {
66 | $response = wp_remote_get($plugin_data['UpdateURI']);
67 | }
68 |
69 | if (is_a($response, 'WP_Error')) {
70 | /** @var WP_Error $response */
71 | error_log('Error updating plugin [Convoworks GPT]: ' . implode("\n", $response->get_error_messages()));
72 | return $update;
73 | }
74 |
75 | if (empty($response['body'])) {
76 | return $update;
77 | }
78 |
79 | $custom_plugins_data = json_decode($response['body'], true);
80 |
81 | if (!empty($custom_plugins_data[$plugin_file])) {
82 | $custom_data = $custom_plugins_data[$plugin_file];
83 | $custom_data['slug'] = $plugin_file; // Add slug property here
84 | return (object) $custom_data;
85 | } else {
86 | return $update;
87 | }
88 | }
89 | add_filter('update_plugins_raw.githubusercontent.com', 'convoworks_gpt_check_for_updates', 10, 3);
90 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/MediaRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_media',
24 | 'description' => 'List WordPress media items with filters.',
25 | 'parameters' => [
26 | 'per_page' => ['type' => 'number', 'description' => 'Media items per page', 'default' => 10],
27 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1]
28 | ],
29 | 'defaults' => ['per_page' => 10, 'page' => 1],
30 | 'required' => [],
31 | 'execute' => $this->_makeRestFn('GET', 'media')
32 | ],
33 | [
34 | 'name' => 'get_media',
35 | 'description' => 'Retrieve a media item by ID.',
36 | 'parameters' => [
37 | 'id' => ['type' => 'number', 'description' => 'Media ID']
38 | ],
39 | 'defaults' => [],
40 | 'required' => ['id'],
41 | 'execute' => function ($data) {
42 | $fn = $this->_makeRestFn('GET', "media/{$data['id']}");
43 | return $fn($data);
44 | }
45 | ],
46 | [
47 | 'name' => 'create_media',
48 | 'description' => 'Upload a new media item.',
49 | 'parameters' => [
50 | 'file' => ['type' => 'string', 'description' => 'File path or URL'],
51 | 'title' => ['type' => 'string', 'description' => 'Media title']
52 | ],
53 | 'defaults' => [],
54 | 'required' => ['file'],
55 | 'execute' => $this->_makeRestFn('POST', 'media')
56 | ],
57 | [
58 | 'name' => 'delete_media',
59 | 'description' => 'Delete a media item.',
60 | 'parameters' => [
61 | 'id' => ['type' => 'number', 'description' => 'Media ID'],
62 | 'force' => ['type' => 'boolean', 'default' => true]
63 | ],
64 | 'defaults' => ['force' => true],
65 | 'required' => ['id'],
66 | 'execute' => function ($data) {
67 | return $this->_makeRestFn('DELETE', "media/{$data['id']}")(['force' => $data['force']]);
68 | }
69 | ]
70 | ];
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/StreamHandler.php:
--------------------------------------------------------------------------------
1 | _streamWriter = $streamWriter;
29 | $this->_logger = $logger;
30 | }
31 |
32 | /**
33 | * Starts and manages the SSE stream.
34 | *
35 | * @param string $sessionId
36 | * @param McpSessionManager $manager
37 | */
38 | public function startSse(string $sessionId, McpSessionManager $manager): void
39 | {
40 | set_time_limit(0);
41 | ignore_user_abort(true);
42 |
43 | if (PHP_VERSION_ID >= 80000) {
44 | ob_implicit_flush(true);
45 | } else {
46 | ob_implicit_flush(1);
47 | }
48 |
49 | while (ob_get_level()) ob_end_clean();
50 |
51 | $headers = [
52 | 'Content-Type' => 'text/event-stream; charset=utf-8',
53 | 'Cache-Control' => 'no-cache',
54 | 'mcp-session-id' => $sessionId,
55 | 'MCP-Protocol-Version' => '2025-06-18'
56 | ];
57 | $this->_streamWriter->sendHeaders($headers);
58 |
59 | $this->_streamWriter->sendEvent('message', ': connected');
60 |
61 | $lastPing = time();
62 | $lastSessionCheck = time();
63 |
64 | while (!connection_aborted()) {
65 | if ((time() - $lastSessionCheck) >= 1) {
66 | try {
67 | $manager->getActiveSession($sessionId, true);
68 | } catch (DataItemNotFoundException $e) {
69 | $this->_logger->warning("Session check failed [$sessionId]: " . $e->getMessage());
70 | break;
71 | }
72 | $lastSessionCheck = time();
73 | }
74 |
75 | if ($message = $manager->getSessionStore()->nextEvent($sessionId)) {
76 | $data = is_string($message['data']) ? $message['data'] : json_encode($message['data']);
77 | $this->_streamWriter->sendEvent('message', $data);
78 | }
79 |
80 | if (CONVO_GPT_MCP_PING_INTERVAL && (time() - $lastPing) >= CONVO_GPT_MCP_PING_INTERVAL) {
81 | $this->_streamWriter->sendPing();
82 | $manager->getSessionStore()->pingSession($sessionId);
83 | $lastPing = time();
84 | }
85 |
86 | usleep(CONVO_GPT_MCP_LISTEN_USLEEP);
87 | }
88 |
89 | $this->_logger->info("SSE disconnected for session [$sessionId]");
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/McpSessionManager.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
25 | $this->_sessionStore = $sessionStore;
26 | }
27 |
28 | // check if valid session
29 | public function getActiveSession($sessionId, $allowNew = false): array
30 | {
31 | $session = $this->_sessionStore->getSession($sessionId);
32 | $status_ok = [
33 | IMcpSessionStoreInterface::SESSION_STATUS_INITIALISED,
34 | ];
35 | if ($allowNew) {
36 | $status_ok[] = IMcpSessionStoreInterface::SESSION_STATUS_NEW;
37 | }
38 | if (!in_array($session['status'], $status_ok)) {
39 | throw new DataItemNotFoundException('No active session found [' . $allowNew . ']: ' . $sessionId);
40 | }
41 | if ($session['last_active'] < time() - CONVO_GPT_MCP_SESSION_TIMEOUT) {
42 | throw new DataItemNotFoundException('Session expired: ' . $sessionId);
43 | }
44 | return $session;
45 | }
46 |
47 | // creates new session and sets up stream headers
48 | public function startSession($clientName): string
49 | {
50 | $session_id = $this->_sessionStore->createSession($clientName);
51 |
52 | $this->_logger->info("New session started: $session_id");
53 |
54 | return $session_id;
55 | }
56 |
57 | public function activateSession($sessionId): array
58 | {
59 | $session = $this->_sessionStore->getSession($sessionId);
60 | if ($session['status'] !== IMcpSessionStoreInterface::SESSION_STATUS_NEW) {
61 | throw new DataItemNotFoundException('No NEW session found: ' . $sessionId);
62 | }
63 |
64 | $this->_sessionStore->initialiseSession($sessionId);
65 |
66 | return $session;
67 | }
68 |
69 | public function terminateSession($sessionId): void
70 | {
71 | $session = $this->_sessionStore->getSession($sessionId);
72 | $session['status'] = IMcpSessionStoreInterface::SESSION_STATUS_TERMINATED;
73 | $session['last_active'] = time();
74 | $this->_sessionStore->saveSession($session);
75 | $this->_logger->info("Session terminated: $sessionId");
76 | }
77 |
78 | // queues the full JSON-RPC message
79 | public function enqueueMessage($sessionId, $message): void
80 | {
81 | $this->_sessionStore->queueEvent($sessionId, $message);
82 | }
83 |
84 | public function getSessionStore(): IMcpSessionStoreInterface
85 | {
86 | return $this->_sessionStore;
87 | }
88 |
89 | // UTIL
90 | public function __toString()
91 | {
92 | return get_class($this) . '[]';
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/.phpstan-setup.md:
--------------------------------------------------------------------------------
1 | # PHPStan WordPress Integration - Setup Complete ✅
2 |
3 | ## What Was Fixed
4 |
5 | ### 1. WordPress Classes and Functions
6 | **Problem**: PHPStan didn't recognize WordPress classes like `WP_REST_Request`, `WP_REST_Response`, etc.
7 |
8 | **Solution**: Installed official WordPress stubs
9 | ```bash
10 | composer require --dev php-stubs/wordpress-stubs
11 | ```
12 |
13 | ### 2. Plugin Constants
14 | **Problem**: PHPStan reported constants like `CONVO_GPT_URL`, `CONVO_GPT_MCP_SESSION_TIMEOUT` as not found.
15 |
16 | **Solution**: Created `phpstan-bootstrap.php` defining all plugin constants.
17 |
18 | ### 3. Convoworks WP Classes
19 | **Problem**: PHPStan didn't know about `Convo\Wp\AdminUser` class from convoworks-wp plugin.
20 |
21 | **Solution**: Created stub file `phpstan-stubs/convoworks-wp.php` with minimal interface implementation.
22 |
23 | ### 4. Wrong PHPDoc Type
24 | **Problem**: `WpRestProxyFunction.php` had incorrect namespace in PHPDoc (`Convo\Gpt\Pckg\WP_REST_Response`).
25 |
26 | **Solution**: Fixed to use global namespace `\WP_REST_Response`.
27 |
28 | ## Results
29 |
30 | - **Before**: 78 errors (many WordPress-related false positives)
31 | - **After**: 60 errors (real code issues to address)
32 | - **Improvement**: Eliminated 18 WordPress/infrastructure errors ✨
33 |
34 | ## Remaining Errors
35 |
36 | The 60 remaining errors are legitimate code issues:
37 | - Missing return statements
38 | - Type mismatches
39 | - Dead code
40 | - Unused properties
41 | - Always-true/false conditions
42 |
43 | These are **real code quality issues** worth fixing, not false positives!
44 |
45 | ## Configuration Files
46 |
47 | ### `phpstan.neon`
48 | ```yaml
49 | parameters:
50 | level: 5
51 | paths:
52 | - src
53 | bootstrapFiles:
54 | - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
55 | - phpstan-bootstrap.php
56 | - phpstan-stubs/convoworks-wp.php
57 | ```
58 |
59 | ### `phpstan-bootstrap.php`
60 | Defines plugin constants for static analysis.
61 |
62 | ### `phpstan-stubs/convoworks-wp.php`
63 | Minimal stub for `Convo\Wp\AdminUser` class.
64 |
65 | ## Usage
66 |
67 | ```bash
68 | # Run PHPStan analysis
69 | vendor/bin/phpstan analyse
70 |
71 | # Run on specific file
72 | vendor/bin/phpstan analyse src/Convo/Gpt/Admin/
73 |
74 | # With error baseline (to ignore existing errors)
75 | vendor/bin/phpstan analyse --generate-baseline
76 | ```
77 |
78 | ## For Other WordPress Plugins
79 |
80 | If you have similar WordPress plugin projects, use the same approach:
81 |
82 | 1. **Install WordPress stubs**:
83 | ```bash
84 | composer require --dev php-stubs/wordpress-stubs
85 | ```
86 |
87 | 2. **Add to phpstan.neon**:
88 | ```yaml
89 | parameters:
90 | bootstrapFiles:
91 | - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
92 | ```
93 |
94 | 3. **Create bootstrap file** for plugin constants
95 |
96 | 4. **Create stub files** for external dependencies (like convoworks-core, convoworks-wp)
97 |
98 | ## Next Steps
99 |
100 | To get to **zero errors**, consider:
101 |
102 | 1. Fix missing return statements
103 | 2. Add proper type hints where PHPStan detects issues
104 | 3. Remove dead code
105 | 4. Fix type mismatches
106 | 5. Consider using PHPStan baseline if some errors are intentional:
107 | ```bash
108 | vendor/bin/phpstan analyse --generate-baseline
109 | ```
110 |
111 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/McpServerCommandRequest.php:
--------------------------------------------------------------------------------
1 | _serviceId = $serviceId;
25 | $this->_sessionId = $sessionId;
26 | $this->_requestId = $requestId;
27 | $this->_data = $data;
28 | $this->_role = $role;
29 | }
30 |
31 | public function getId()
32 | {
33 | return $this->_data['id'] ?? null;
34 | }
35 |
36 | public function getMethod()
37 | {
38 | return $this->_data['method'] ?? null;
39 | }
40 |
41 | public function getSpecialRole()
42 | {
43 | return $this->_role;
44 | }
45 |
46 | public function getServiceId()
47 | {
48 | return $this->_serviceId;
49 | }
50 |
51 | public function getPlatformId()
52 | {
53 | return McpServerPlatform::PLATFORM_ID;
54 | }
55 |
56 | public function getApplicationId()
57 | {
58 | return $this->getServiceId();
59 | }
60 |
61 | public function getPlatformData()
62 | {
63 | return $this->_data;
64 | }
65 |
66 | public function getMediaTypeRequest(): string
67 | {
68 | return '';
69 | }
70 |
71 | public function getText()
72 | {
73 | return '';
74 | }
75 |
76 | public function isEmpty()
77 | {
78 | return false;
79 | }
80 |
81 | public function getInstallationId()
82 | {
83 | return $this->getServiceId();
84 | }
85 |
86 | public function isMediaRequest()
87 | {
88 | return false;
89 | }
90 |
91 | public function isHealthCheck()
92 | {
93 | return false;
94 | }
95 |
96 | public function getIsCrossSessionCapable()
97 | {
98 | return true;
99 | }
100 |
101 | public function getAccessToken(): string
102 | {
103 | return '';
104 | }
105 |
106 | public function isLaunchRequest()
107 | {
108 | return $this->isSessionStart();
109 | }
110 |
111 | public function getDeviceId()
112 | {
113 | return $this->getServiceId();
114 | }
115 |
116 | public function isSalesRequest()
117 | {
118 | return false;
119 | }
120 |
121 | public function isSessionEndRequest(): bool
122 | {
123 | return false;
124 | }
125 |
126 | public function isSessionStart()
127 | {
128 | return false;
129 | }
130 |
131 | public function getSessionId()
132 | {
133 | return $this->_sessionId;
134 | }
135 |
136 | public function getRequestId()
137 | {
138 | return $this->_requestId;
139 | }
140 |
141 |
142 |
143 | // UTIL
144 | public function __toString()
145 | {
146 | return get_class($this) . '[' . $this->getText() . '][' . $this->getServiceId() . '][' . $this->getRequestId() . '][' . $this->getSessionId() . '][' . $this->getSpecialRole() . ']';
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/PagesRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_pages',
24 | 'description' => 'List WordPress pages with filters.',
25 | 'parameters' => [
26 | 'status' => ['type' => 'string', 'description' => 'Page status', 'default' => 'publish'],
27 | 'per_page' => ['type' => 'number', 'description' => 'Pages per page', 'default' => 10],
28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1]
29 | ],
30 | 'defaults' => ['status' => 'publish', 'per_page' => 10, 'page' => 1],
31 | 'required' => [],
32 | 'execute' => $this->_makeRestFn('GET', 'pages')
33 | ],
34 | [
35 | 'name' => 'get_page',
36 | 'description' => 'Retrieve a page by ID.',
37 | 'parameters' => [
38 | 'id' => ['type' => 'number', 'description' => 'Page ID']
39 | ],
40 | 'defaults' => [],
41 | 'required' => ['id'],
42 | 'execute' => function ($data) {
43 | return $this->_makeRestFn('GET', "pages/{$data['id']}")($data);
44 | }
45 | ],
46 | [
47 | 'name' => 'create_page',
48 | 'description' => 'Create a new page.',
49 | 'parameters' => [
50 | 'title' => ['type' => 'string', 'description' => 'Page title'],
51 | 'content' => ['type' => 'string', 'description' => 'Page content'],
52 | 'status' => ['type' => 'string', 'enum' => ['publish', 'draft'], 'default' => 'draft']
53 | ],
54 | 'defaults' => ['status' => 'draft'],
55 | 'required' => ['title', 'content'],
56 | 'execute' => $this->_makeRestFn('POST', 'pages')
57 | ],
58 | [
59 | 'name' => 'update_page',
60 | 'description' => 'Update an existing page.',
61 | 'parameters' => [
62 | 'id' => ['type' => 'number', 'description' => 'Page ID'],
63 | 'title' => ['type' => 'string'],
64 | 'content' => ['type' => 'string']
65 | ],
66 | 'defaults' => [],
67 | 'required' => ['id'],
68 | 'execute' => function ($data) {
69 | $id = $data['id'];
70 | unset($data['id']);
71 | return $this->_makeRestFn('POST', "pages/{$id}")($data);
72 | }
73 | ],
74 | [
75 | 'name' => 'delete_page',
76 | 'description' => 'Delete a page.',
77 | 'parameters' => [
78 | 'id' => ['type' => 'number', 'description' => 'Page ID'],
79 | 'force' => ['type' => 'boolean', 'default' => true]
80 | ],
81 | 'defaults' => ['force' => true],
82 | 'required' => ['id'],
83 | 'execute' => function ($data) {
84 | return $this->_makeRestFn('DELETE', "pages/{$data['id']}")(['force' => $data['force']]);
85 | }
86 | ]
87 | ];
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/CommentRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_comments',
24 | 'description' => 'List WordPress comments with filters.',
25 | 'parameters' => [
26 | 'post' => ['type' => 'number', 'description' => 'Post ID'],
27 | 'per_page' => ['type' => 'number', 'description' => 'Comments per page', 'default' => 10],
28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1]
29 | ],
30 | 'defaults' => ['per_page' => 10, 'page' => 1],
31 | 'required' => [],
32 | 'execute' => $this->_makeRestFn('GET', 'comments')
33 | ],
34 | [
35 | 'name' => 'get_comment',
36 | 'description' => 'Retrieve a comment by ID.',
37 | 'parameters' => [
38 | 'id' => ['type' => 'number', 'description' => 'Comment ID']
39 | ],
40 | 'defaults' => [],
41 | 'required' => ['id'],
42 | 'execute' => function ($data) {
43 | $fn = $this->_makeRestFn('GET', "comments/{$data['id']}");
44 | return $fn($data);
45 | }
46 | ],
47 | [
48 | 'name' => 'create_comment',
49 | 'description' => 'Create a new comment.',
50 | 'parameters' => [
51 | 'post' => ['type' => 'number', 'description' => 'Post ID'],
52 | 'content' => ['type' => 'string', 'description' => 'Comment content'],
53 | 'author' => ['type' => 'string', 'description' => 'Author name'],
54 | 'author_email' => ['type' => 'string', 'description' => 'Author email']
55 | ],
56 | 'defaults' => [],
57 | 'required' => ['post', 'content', 'author', 'author_email'],
58 | 'execute' => $this->_makeRestFn('POST', 'comments')
59 | ],
60 | [
61 | 'name' => 'update_comment',
62 | 'description' => 'Update an existing comment.',
63 | 'parameters' => [
64 | 'id' => ['type' => 'number', 'description' => 'Comment ID'],
65 | 'content' => ['type' => 'string']
66 | ],
67 | 'defaults' => [],
68 | 'required' => ['id'],
69 | 'execute' => function ($data) {
70 | $id = $data['id'];
71 | unset($data['id']);
72 | $fn = $this->_makeRestFn('POST', "comments/{$id}");
73 | return $fn($data);
74 | }
75 | ],
76 | [
77 | 'name' => 'delete_comment',
78 | 'description' => 'Delete a comment.',
79 | 'parameters' => [
80 | 'id' => ['type' => 'number', 'description' => 'Comment ID'],
81 | 'force' => ['type' => 'boolean', 'default' => true]
82 | ],
83 | 'defaults' => ['force' => true],
84 | 'required' => ['id'],
85 | 'execute' => function ($data) {
86 | return $this->_makeRestFn('DELETE', "comments/{$data['id']}")(['force' => $data['force']]);
87 | }
88 | ]
89 | ];
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/UserRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
15 | return $functions;
16 | }, 10, 2);
17 | }
18 |
19 | private function _buildFunctions()
20 | {
21 | return [
22 | [
23 | 'name' => 'list_users',
24 | 'description' => 'List WordPress users with filters.',
25 | 'parameters' => [
26 | 'role' => ['type' => 'string', 'description' => 'User role', 'default' => 'subscriber'],
27 | 'per_page' => ['type' => 'number', 'description' => 'Users per page', 'default' => 10],
28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1]
29 | ],
30 | 'defaults' => ['role' => 'subscriber', 'per_page' => 10, 'page' => 1],
31 | 'required' => [],
32 | 'execute' => $this->_makeRestFn('GET', 'users')
33 | ],
34 | [
35 | 'name' => 'get_user',
36 | 'description' => 'Retrieve a user by ID.',
37 | 'parameters' => [
38 | 'id' => ['type' => 'number', 'description' => 'User ID']
39 | ],
40 | 'defaults' => [],
41 | 'required' => ['id'],
42 | 'execute' => function ($data) {
43 | $fn = $this->_makeRestFn('GET', "users/{$data['id']}");
44 | return $fn($data);
45 | }
46 | ],
47 | [
48 | 'name' => 'create_user',
49 | 'description' => 'Create a new user.',
50 | 'parameters' => [
51 | 'username' => ['type' => 'string', 'description' => 'Username'],
52 | 'email' => ['type' => 'string', 'description' => 'User email'],
53 | 'password' => ['type' => 'string', 'description' => 'User password'],
54 | 'role' => ['type' => 'string', 'default' => 'subscriber']
55 | ],
56 | 'defaults' => ['role' => 'subscriber'],
57 | 'required' => ['username', 'email', 'password'],
58 | 'execute' => $this->_makeRestFn('POST', 'users')
59 | ],
60 | [
61 | 'name' => 'update_user',
62 | 'description' => 'Update an existing user.',
63 | 'parameters' => [
64 | 'id' => ['type' => 'number', 'description' => 'User ID'],
65 | 'email' => ['type' => 'string'],
66 | 'role' => ['type' => 'string']
67 | ],
68 | 'defaults' => [],
69 | 'required' => ['id'],
70 | 'execute' => function ($data) {
71 | $id = $data['id'];
72 | unset($data['id']);
73 | $fn = $this->_makeRestFn('POST', "users/{$id}");
74 | return $fn($data);
75 | }
76 | ],
77 | [
78 | 'name' => 'delete_user',
79 | 'description' => 'Delete a user.',
80 | 'parameters' => [
81 | 'id' => ['type' => 'number', 'description' => 'User ID'],
82 | 'reassign' => ['type' => 'number', 'description' => 'Reassign posts to this user ID']
83 | ],
84 | 'defaults' => [],
85 | 'required' => ['id'],
86 | 'execute' => function ($data) {
87 | return $this->_makeRestFn('DELETE', "users/{$data['id']}")(['reassign' => $data['reassign']]);
88 | }
89 | ]
90 | ];
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/GptPlugin.php:
--------------------------------------------------------------------------------
1 | _pluginContext)) {
33 | throw new \Exception('Not properly iinitilaized');
34 | }
35 | return $this->_pluginContext;
36 | }
37 |
38 | public function register()
39 | {
40 | add_action('init', [$this, 'init']);
41 | }
42 |
43 | public function init()
44 | {
45 | if (!defined('CONVOWP_VERSION')) {
46 | error_log('GPT: Convoworks WP is not present. Exiting ...');
47 |
48 | add_action('admin_notices', function () {
49 | echo '
50 |
Convoworks GPT requires Convoworks WP plugin to be installed and activated
51 |
';
52 | });
53 | return;
54 | }
55 |
56 | add_action('admin_init', [$this, 'adminInit']);
57 |
58 | add_action('admin_menu', function () {
59 | $context = $this->getPluginContext();
60 | $settings_view = new SettingsView(
61 | $context->getLogger(),
62 | $context->getSettingsViewModel(),
63 | $context->getMcpConvoworksManager()
64 | );
65 | $settings_view->register();
66 | }, 20);
67 | $this->_pluginContext = new PluginContext();
68 | $this->_pluginContext->init();
69 |
70 | add_action('register_convoworks_package', [$this, 'gptPackageRegister'], 10, 2);
71 |
72 | $posts = new PostRestFunctions();
73 | $posts->register();
74 |
75 | $pages = new PagesRestFunctions();
76 | $pages->register();
77 |
78 | $comments = new CommentRestFunctions();
79 | $comments->register();
80 |
81 | $users = new UserRestFunctions();
82 | $users->register();
83 |
84 | $media = new MediaRestFunctions();
85 | $media->register();
86 |
87 | $plugins = new PluginRestFunctions();
88 | $plugins->register();
89 |
90 | $taxonomies = new TaxonomyRestFunctions();
91 | $taxonomies->register();
92 |
93 | $settings = new SettingsRestFunctions();
94 | $settings->register();
95 | }
96 |
97 |
98 | public function adminInit()
99 | {
100 | $context = $this->getPluginContext();
101 | $logger = $context->getLogger();
102 |
103 | // $logger->debug('Admin init ...');
104 |
105 | if (!empty($_POST) && isset($_REQUEST['action'])) {
106 | $processor = new SettingsProcessor($logger, $context->getSettingsViewModel(), $context->getMcpConvoworksManager());
107 | if ($processor->accepts()) {
108 | $logger->debug('Processing settings ...');
109 | $processor->run();
110 | }
111 | }
112 | }
113 |
114 | /**
115 | * @param PackageProviderFactory $packageProviderFactory
116 | * @param ContainerInterface $container
117 | */
118 | public function gptPackageRegister($packageProviderFactory, $container)
119 | {
120 | $packageProviderFactory->registerPackage($this->getPluginContext()->getMcpServerPackage());
121 | }
122 |
123 | // UTIL
124 | public function __toString()
125 | {
126 | return get_class($this);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/ExternalChatFunctionElement.php:
--------------------------------------------------------------------------------
1 | _name = $properties['name'];
30 | $this->_description = $properties['description'];
31 | $this->_parameters = $properties['parameters'];
32 | $this->_defaults = $properties['defaults'] ?? [];
33 | $this->_required = $properties['required'];
34 | $this->_execute = $properties['execute'];
35 | }
36 |
37 | public function read(IConvoRequest $request, IConvoResponse $response)
38 | {
39 | /** @var \Convo\Gpt\IChatFunctionContainer $container */
40 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer');
41 |
42 | $data = [
43 | 'name' => $this->evaluateString($this->_name),
44 | 'description' => $this->evaluateString($this->_description),
45 | 'parameters' => $this->evaluateString($this->_parameters),
46 | 'defaults' => $this->evaluateString($this->_defaults),
47 | 'required' => $this->evaluateString($this->_required),
48 | 'execute' => $this->evaluateString($this->_execute),
49 | ];
50 |
51 | $chat_function = new class($data, $this->getService()) extends AbstractScopedFunction implements IChatFunction {
52 |
53 | /**
54 | * @var ConvoServiceInstance
55 | */
56 | private $_convoServiceInstance;
57 | private $_functionData;
58 | public function __construct($functionData, $service)
59 | {
60 | $this->_functionData = $functionData;
61 | $this->_convoServiceInstance = $service;
62 | }
63 |
64 | public function accepts($functionName)
65 | {
66 | return $functionName === $this->getName();
67 | }
68 |
69 | public function getName()
70 | {
71 | return $this->_functionData['name'];
72 | }
73 |
74 | public function getDefinition()
75 | {
76 | // Return the definition of the function, as an array
77 |
78 | return [
79 | 'name' => $this->getName(),
80 | 'description' => $this->_functionData['description'],
81 | 'parameters' => [
82 | 'type' => 'object',
83 | 'properties' => empty($this->_functionData['parameters']) ? new \stdClass : $this->_functionData['parameters'],
84 | 'required' => $this->_functionData['required'] ?? [],
85 | ],
86 | ];
87 | }
88 |
89 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data)
90 | {
91 |
92 | // $data = json_decode($data, true);
93 | $data = array_merge($this->_functionData['defaults'], $data);
94 | $result = $this->_functionData['execute']($data);
95 |
96 | if (is_string($result)) {
97 | return $result;
98 | }
99 | return json_encode($result);
100 | }
101 | };
102 |
103 | $container->registerFunction($chat_function);
104 | }
105 |
106 |
107 | // UTIL
108 | public function __toString()
109 | {
110 | return parent::__toString() . '[' . $this->_name . ']';
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/ProcessJsonWithConstantsTest.php:
--------------------------------------------------------------------------------
1 | \n Order allow,deny\n Deny from all\n\n", "FILE_APPEND"]',
50 | '["\/home\/fohovotaha8776\/web\/zealous-zebra-1ckjj.instawp.xyz\/public_html\/.htaccess","# Prevent access to debug.log\n\n Order allow,deny\n Deny from all\n<\/Files>\n",8]'
51 | ],
52 | [
53 | '[\n \"C:\\\\xampp\\\\htdocs\\\\wp-test\\\\.htaccess\",\n \"# BEGIN WordPress\\n# The directives (lines) between \\\"BEGIN WordPress\\\" and \\\"END WordPress\\\" are\\n# dynamically generated, and should only be modified via WordPress filters.\\n# Any changes to the directives between these markers will be overwritten.\\n\\nRewriteEngine On\\nRewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\\nRewriteBase \/wp-test\/\\nRewriteRule ^index\\\\.php$ - [L]\\nRewriteCond %{REQUEST_FILENAME} !-f\\nRewriteCond %{REQUEST_FILENAME} !-d\\nRewriteRule . \/wp-test\/index.php [L]\\n<\/IfModule>\\n# END WordPress\\n\\n# BEGIN Restrict access to debug.log\\n\\nOrder allow,deny\\nDeny from all\\n<\/Files>\\n# END Restrict access to debug.log\\n\"\n ]',
54 | '[\n \"C:\\\\xampp\\\\htdocs\\\\wp-test\\\\.htaccess\",\n \"# BEGIN WordPress\\n# The directives (lines) between \\\"BEGIN WordPress\\\" and \\\"END WordPress\\\" are\\n# dynamically generated, and should only be modified via WordPress filters.\\n# Any changes to the directives between these markers will be overwritten.\\n\\nRewriteEngine On\\nRewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\\nRewriteBase \/wp-test\/\\nRewriteRule ^index\\\\.php$ - [L]\\nRewriteCond %{REQUEST_FILENAME} !-f\\nRewriteCond %{REQUEST_FILENAME} !-d\\nRewriteRule . \/wp-test\/index.php [L]\\n<\/IfModule>\\n# END WordPress\\n\\n# BEGIN Restrict access to debug.log\\n\\nOrder allow,deny\\nDeny from all\\n<\/Files>\\n# END Restrict access to debug.log\\n\"\n ]'
55 | ]
56 | ];
57 | }
58 |
59 | /**
60 | * @dataProvider processDataProvider
61 | */
62 | public function testProcessJson($inputJson, $expectedJson)
63 | {
64 | $result = Util::processJsonWithConstants($inputJson);
65 |
66 | $this->assertEquals($expectedJson, $result, "Failed asserting that two objects are equal.");
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Admin/SettingsViewModel.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
32 | $this->_convoServiceDataProvider = $convoServiceDataProvider;
33 | }
34 |
35 | public function init()
36 | {
37 | $page = isset($_REQUEST['page']) ? sanitize_text_field(wp_unslash($_REQUEST['page'])) : '';
38 | if ($page !== SettingsView::ID) {
39 | // $this->_logger->debug('Not convoworks mcp server call. Exiting ...');
40 | return;
41 | }
42 |
43 | $this->_logger->info('Initializing convoworks mcp settings view model');
44 |
45 | // Load basicAuth from config
46 | try {
47 | $svcId = $this->getSelectedServiceId();
48 | $user = new \Convo\Wp\AdminUser(wp_get_current_user());
49 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig(
50 | $user,
51 | $svcId,
52 | \Convo\Core\Publish\IPlatformPublisher::MAPPING_TYPE_DEVELOP
53 | );
54 | $platformId = \Convo\Gpt\Mcp\McpServerPlatform::PLATFORM_ID;
55 | if (isset($config[$platformId]['basic_auth'])) {
56 | $this->_basicAuth = $config[$platformId]['basic_auth'] ? true : false;
57 | }
58 | } catch (\Throwable $e) {
59 | /** @phpstan-ignore-next-line */
60 | $this->_logger->error($e);
61 | }
62 | }
63 |
64 | public function getSelectedServiceId()
65 | {
66 | if (isset($_REQUEST['service_id'])) {
67 | return sanitize_text_field(wp_unslash($_REQUEST['service_id']));
68 | }
69 | throw new DataItemNotFoundException('No selected service found');
70 | }
71 |
72 | public function hasServiceSelection()
73 | {
74 | try {
75 | $this->getSelectedServiceId();
76 | return true;
77 | } catch (DataItemNotFoundException $e) {
78 | }
79 | return false;
80 | }
81 |
82 | public function getPageId()
83 | {
84 | return SettingsView::ID;
85 | }
86 |
87 | public function getBaseUrl($serviceId = null)
88 | {
89 | $args = [
90 | 'page' => $this->getPageId(),
91 | ];
92 | if ($serviceId) {
93 | $args['service_id'] = $serviceId;
94 | }
95 | $url = add_query_arg($args, admin_url('admin.php'));
96 | return $url;
97 | }
98 |
99 | public function getActionUrl($action, $params = [])
100 | {
101 | $url = add_query_arg(array_merge([
102 | 'page' => $this->getPageId(),
103 | 'action' => $action,
104 | ], $params), admin_url('admin-post.php'));
105 | $url = wp_nonce_url($url, self::NONCE_ACTION, self::NONCE_NAME);
106 | return $url;
107 | }
108 |
109 | public function getBackToConvoworksUrl()
110 | {
111 | $url = add_query_arg([
112 | 'page' => 'convo-plugin',
113 | ], admin_url('admin.php'));
114 | $url .= '#!/convoworks-editor/' . $this->getSelectedServiceId() . '/configuration/platforms';
115 | return $url;
116 | }
117 |
118 | public function getFormUrl($params = [])
119 | {
120 | $url = add_query_arg(array_merge([
121 | 'page' => $this->getPageId(),
122 | ], $params), admin_url('admin-post.php'));
123 | return $url;
124 | }
125 |
126 | public function getBasicAuth()
127 | {
128 | return $this->_basicAuth;
129 | }
130 |
131 | public function setBasicAuth($value)
132 | {
133 | $this->_basicAuth = $value ? true : false;
134 | }
135 |
136 | // UTIL
137 | public function __toString()
138 | {
139 | return get_class($this);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Tools/PostRestFunctions.php:
--------------------------------------------------------------------------------
1 | _buildFunctions());
16 | return $functions;
17 | }, 10, 2);
18 | }
19 |
20 | private function _buildFunctions()
21 | {
22 | return [
23 | [
24 | 'name' => 'list_posts',
25 | 'description' => 'List WordPress posts with filters.',
26 | 'parameters' => [
27 | 'status' => ['type' => 'string', 'description' => 'Post status', 'default' => 'publish'],
28 | 'per_page' => ['type' => 'number', 'description' => 'Posts per page', 'default' => 10],
29 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1]
30 | ],
31 | 'defaults' => ['status' => 'publish', 'per_page' => 10, 'page' => 1],
32 | 'required' => [],
33 | 'execute' => $this->_makeRestFn('GET', 'posts')
34 | ],
35 | [
36 | 'name' => 'get_post',
37 | 'description' => 'Retrieve a post by ID.',
38 | 'parameters' => [
39 | 'id' => ['type' => 'number', 'description' => 'Post ID']
40 | ],
41 | 'defaults' => [],
42 | 'required' => ['id'],
43 | 'execute' => function ($data) {
44 | $fn = $this->_makeRestFn('GET', "posts/{$data['id']}");
45 | return $fn($data);
46 | }
47 | ],
48 | [
49 | 'name' => 'create_post',
50 | 'description' => 'Create a new post.',
51 | 'parameters' => [
52 | 'title' => ['type' => 'string', 'description' => 'Post title'],
53 | 'content' => ['type' => 'string', 'description' => 'Post content'],
54 | 'status' => ['type' => 'string', 'enum' => ['publish', 'draft'], 'default' => 'draft']
55 | ],
56 | 'defaults' => ['status' => 'draft'],
57 | 'required' => ['title', 'content'],
58 | 'execute' => $this->_makeRestFn('POST', 'posts')
59 | ],
60 | [
61 | 'name' => 'update_post',
62 | 'description' => 'Update an existing post.',
63 | 'parameters' => [
64 | 'id' => ['type' => 'number', 'description' => 'Post ID'],
65 | 'title' => ['type' => 'string'],
66 | 'content' => ['type' => 'string']
67 | ],
68 | 'defaults' => [],
69 | 'required' => ['id'],
70 | 'execute' => function ($data) {
71 | $id = $data['id'];
72 | unset($data['id']);
73 | $fn = $this->_makeRestFn('GET', "posts/{$id}");
74 | return $fn($data);
75 | }
76 | ],
77 | [
78 | 'name' => 'delete_post',
79 | 'description' => 'Delete a post.',
80 | 'parameters' => [
81 | 'id' => ['type' => 'number', 'description' => 'Post ID'],
82 | 'force' => ['type' => 'boolean', 'default' => true]
83 | ],
84 | 'defaults' => ['force' => true],
85 | 'required' => ['id'],
86 | 'execute' => function ($data) {
87 | return $this->_makeRestFn('DELETE', "posts/{$data['id']}")(['force' => $data['force']]);
88 | }
89 | ],
90 | [
91 | 'name' => 'search_posts',
92 | 'description' => 'Search posts by title or content.',
93 | 'parameters' => [
94 | 'search' => ['type' => 'string', 'description' => 'Search query'],
95 | 'per_page' => ['type' => 'number', 'default' => 5]
96 | ],
97 | 'defaults' => ['per_page' => 5],
98 | 'required' => ['search'],
99 | 'execute' => $this->_makeRestFn('GET', 'posts')
100 | ]
101 | ];
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/ChatFunctionElement.php:
--------------------------------------------------------------------------------
1 | _name = $properties['name'];
35 | $this->_description = $properties['description'];
36 | $this->_parameters = $properties['parameters'];
37 | $this->_defaults = $properties['defaults'] ?? [];
38 | $this->_required = $properties['required'];
39 | $this->_requestData = $properties['request_data'];
40 | $this->_resultData = $properties['result_data'];
41 |
42 | foreach ($properties['ok'] as $element) {
43 | $this->_ok[] = $element;
44 | $this->addChild($element);
45 | }
46 | }
47 |
48 | public function read(IConvoRequest $request, IConvoResponse $response)
49 | {
50 | /** @var \Convo\Gpt\IChatFunctionContainer $container */
51 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer');
52 | $container->registerFunction($this);
53 | }
54 |
55 | /**
56 | * @param IConvoRequest $request
57 | * @param IConvoResponse $response
58 | * @param array $data
59 | * @return string
60 | */
61 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data)
62 | {
63 | // $data = json_decode($data, true);
64 | // $error = json_last_error();
65 | // if ($error !== JSON_ERROR_NONE) {
66 | // throw new \Exception('JSON parsing error: ' . json_last_error_msg());
67 | // }
68 | $this->_logger->debug('Got data decoded [' . print_r($data, true) . ']');
69 | $data = array_merge($this->_getDefaults(), $data);
70 | $this->_logger->info('Got data with defaults [' . print_r($data, true) . ']');
71 |
72 | $params = $this->getFunctionParams();
73 | $data_var = $this->evaluateString($this->_requestData);
74 | $params->setServiceParam($data_var, $data);
75 |
76 | $this->_logger->info('Executing function [' . $this->getName() . ']. Arguments available as [' . $data_var . ']');
77 |
78 | foreach ($this->_ok as $elem) {
79 | $elem->read($request, $response);
80 | }
81 |
82 | // $params = $this->getService()->getServiceParams(IServiceParamsScope::SCOPE_TYPE_REQUEST);
83 | $result = $this->evaluateString($this->_resultData);
84 | if (is_callable($result)) {
85 | $this->_logger->info('Executing callable result');
86 | $result = $result();
87 | }
88 |
89 | if (\is_string($result)) {
90 | return $result;
91 | }
92 | return json_encode($result);
93 | }
94 |
95 | private function _getDefaults()
96 | {
97 | $defaults = $this->evaluateString($this->_defaults);
98 | if (\is_array($defaults)) {
99 | return $defaults;
100 | }
101 | return [];
102 | }
103 |
104 | public function accepts($functionName)
105 | {
106 | return $this->getName() === $functionName;
107 | }
108 |
109 | public function getName()
110 | {
111 | return $this->evaluateString($this->_name);
112 | }
113 |
114 | public function getDefinition()
115 | {
116 | $parameters = $this->getService()->evaluateArgs($this->_parameters, $this);
117 | if (empty($parameters)) {
118 | $parameters = new \stdClass();
119 | }
120 | return [
121 | 'name' => $this->getName(),
122 | 'description' => $this->evaluateString($this->_description),
123 | 'parameters' => [
124 | 'type' => 'object',
125 | 'properties' => $parameters,
126 | 'required' => $this->evaluateString($this->_required),
127 | ],
128 | ];
129 | }
130 |
131 | // UTIL
132 | public function __toString()
133 | {
134 | return parent::__toString() . '[' . $this->_name . ']';
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/GptApi.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
31 | $this->_httpFactory = $httpFactory;
32 | $this->_apiKey = $apiKey;
33 | $this->_baseUrl = empty($baseUrl) ? self::DEFAULT_BASE_URL : $baseUrl;
34 | $this->_logger->debug('GptApi created [' . $this->_baseUrl . ']');
35 | }
36 |
37 | public function chatCompletion($data)
38 | {
39 | $apiUrl = $this->_baseUrl . '/chat/completions';
40 |
41 | $this->_logger->info('Performing request to [' . $apiUrl . ']');
42 |
43 | $headers = [
44 | 'Authorization' => 'Bearer ' . $this->_apiKey,
45 | ];
46 |
47 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']');
48 |
49 | $config = [];
50 |
51 | $client = $this->_httpFactory->getHttpClient($config);
52 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $apiUrl, $headers, $data);
53 |
54 | try {
55 | $response = $client->sendRequest($request);
56 | } catch (HttpClientException $e) {
57 | if ($e->getCode() === 400) {
58 | $response_data = json_decode($e->getMessage(), true);
59 | if (isset($response_data['error']['code']) && $response_data['error']['code'] === 'context_length_exceeded') {
60 | throw new ContextLengthExceededException($response_data['error']['message'], 0, $e);
61 | }
62 | }
63 | throw $e;
64 | }
65 |
66 | $response_data = json_decode($response->getBody()->getContents(), true);
67 |
68 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']');
69 |
70 | return $response_data;
71 | }
72 |
73 |
74 | public function embeddings($data)
75 | {
76 | $headers = [
77 | 'Authorization' => 'Bearer ' . $this->_apiKey,
78 | ];
79 |
80 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']');
81 |
82 | $config = [];
83 |
84 | $client = $this->_httpFactory->getHttpClient($config);
85 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/embeddings', $headers, $data);
86 |
87 | $response = $client->sendRequest($request);
88 |
89 | $response_data = json_decode($response->getBody()->getContents(), true);
90 |
91 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']');
92 |
93 | return $response_data;
94 | }
95 |
96 | public function moderations($data)
97 | {
98 | $headers = [
99 | 'Authorization' => 'Bearer ' . $this->_apiKey,
100 | ];
101 |
102 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']');
103 |
104 | $config = [];
105 |
106 | $client = $this->_httpFactory->getHttpClient($config);
107 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/moderations', $headers, $data);
108 |
109 | $response = $client->sendRequest($request);
110 |
111 | $response_data = json_decode($response->getBody()->getContents(), true);
112 |
113 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']');
114 |
115 | return $response_data;
116 | }
117 |
118 | public function createImage($data)
119 | {
120 | $headers = [
121 | 'Authorization' => 'Bearer ' . $this->_apiKey,
122 | ];
123 |
124 | $this->_logger->debug('Performing request');
125 |
126 | $config = [];
127 |
128 | $client = $this->_httpFactory->getHttpClient($config);
129 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/images/generations', $headers, $data);
130 |
131 | $response = $client->sendRequest($request);
132 |
133 | $response_data = json_decode($response->getBody()->getContents(), true);
134 |
135 | $this->_logger->debug('Http response [' . $response->getStatusCode() . ']');
136 |
137 | return $response_data;
138 | }
139 |
140 | // UTIL
141 | public function __toString()
142 | {
143 | return \get_class($this) . '[' . $this->_baseUrl . ']';
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/SimpleMessagesLimiterElement.php:
--------------------------------------------------------------------------------
1 | _messagesDefinition[] = $element;
38 | $this->addChild($element);
39 | }
40 | }
41 |
42 | if (isset($properties['truncated_flow'])) {
43 | foreach ($properties['truncated_flow'] as $element) {
44 | $this->_truncatedFlow[] = $element;
45 | $this->addChild($element);
46 | }
47 | }
48 | }
49 |
50 | public function registerMessage($message)
51 | {
52 | $this->_messages[] = $message;
53 | }
54 |
55 | public function getMessages()
56 | {
57 | return $this->_messages;
58 | }
59 |
60 | public function read(IConvoRequest $request, IConvoResponse $response)
61 | {
62 | // REGISTER MESSAGES
63 | $this->_messages = [];
64 | foreach ($this->_messagesDefinition as $elem) {
65 | $elem->read($request, $response);
66 | }
67 |
68 | // TRUNCATE
69 | $all_messages = $this->getMessages();
70 | $this->_logger->debug('Checking messages count [' . count($all_messages) . ']');
71 |
72 | $messages = $this->_truncateMessages($all_messages);
73 | $this->_logger->debug('Messages after truncation [' . count($messages) . ']');
74 |
75 | $truncated = Util::getTruncatedPart($all_messages, $messages);
76 |
77 | // TRUNCATED FLOW
78 | if (\count($truncated)) {
79 | $this->_logger->debug('Got messages after truncation [' . print_r($messages, true) . ']. Executing truncated flow');
80 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this);
81 | $var_name = $this->evaluateString($this->_properties['result_var']);
82 | if ($var_name) {
83 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), [
84 | 'messages' => $messages,
85 | 'truncated' => $truncated,
86 | ]);
87 | }
88 |
89 | foreach ($this->_truncatedFlow as $elem) {
90 | $elem->read($request, $response);
91 | }
92 | } else {
93 | $this->_logger->debug('No need tp truncate messages count [' . count($messages) . ']');
94 | }
95 |
96 | // PASS MESSAGES TO THE UPPER LAYER
97 | /** @var \Convo\Gpt\IMessages $container */
98 | $container = $this->findAncestor('\Convo\Gpt\IMessages');
99 |
100 | foreach ($messages as $message) {
101 | $container->registerMessage($message);
102 | }
103 | }
104 |
105 | protected function _truncateMessages($messages)
106 | {
107 | if (isset($this->_properties['max_tokens']) && !empty($this->_properties['max_tokens'])) {
108 | $this->_logger->warning('Checking messages size in tokens [' . count($messages) . '][' . Util::estimateTokensForMessages($messages) . ']');
109 | $messages = Util::truncateByTokens(
110 | $messages,
111 | intval($this->evaluateString($this->_properties['max_tokens'])),
112 | intval($this->evaluateString($this->_properties['truncate_to_tokens']))
113 | );
114 | } else {
115 | $this->_logger->warning('Compatibility mode. No max_tokens set, using max_count and truncate_to');
116 | $messages = Util::truncate(
117 | $messages,
118 | intval($this->evaluateString($this->_properties['max_count'])),
119 | intval($this->evaluateString($this->_properties['truncate_to']))
120 | );
121 | }
122 |
123 | return $messages;
124 | }
125 |
126 | // UTIL
127 | public function __toString()
128 | {
129 | return parent::__toString() . '[]';
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Admin/SettingsProcessor.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
30 | $this->_viewModel = $viewModel;
31 | $this->_mcpManager = $mcpManager;
32 | }
33 |
34 | public function accepts()
35 | {
36 | $action = $this->getAction();
37 | return in_array($action, [
38 | self::ACTION_ENABLE,
39 | self::ACTION_UPDATE, // if you ever want to support updates
40 | self::ACTION_DISABLE, // ← now accepted
41 | ], true);
42 | }
43 |
44 | public function getAction()
45 | {
46 | if (isset($_REQUEST['action'])) {
47 | return sanitize_text_field(wp_unslash($_REQUEST['action']));
48 | }
49 | return '';
50 | }
51 |
52 | public function run()
53 | {
54 | // Verify user has permission
55 | if (!current_user_can('manage_convoworks')) {
56 | wp_die(
57 | esc_html__('You do not have sufficient permissions to access this page.', 'convoworks-gpt'),
58 | esc_html__('Permission Denied', 'convoworks-gpt'),
59 | ['response' => 403]
60 | );
61 | }
62 |
63 | // Verify nonce
64 | if (!check_admin_referer(self::NONCE_ACTION, self::NONCE_NAME)) {
65 | wp_die(
66 | esc_html__('Security check failed. Please try again.', 'convoworks-gpt'),
67 | esc_html__('Security Error', 'convoworks-gpt'),
68 | ['response' => 403]
69 | );
70 | }
71 |
72 | $svc = $this->_viewModel->getSelectedServiceId();
73 | $this->_logger->info("Processing action [{$this->getAction()}] for service [{$svc}]");
74 |
75 | try {
76 | switch ($this->getAction()) {
77 | case self::ACTION_ENABLE:
78 | $this->_processCreate($svc);
79 | $msg = __('MCP service configuration has been successfully created!', 'convoworks-gpt');
80 | $type = 'success';
81 | break;
82 | case self::ACTION_DISABLE:
83 | $this->_processDisable($svc);
84 | $msg = __('MCP service configuration has been successfully disabled!', 'convoworks-gpt');
85 | $type = 'success';
86 | break;
87 | case self::ACTION_UPDATE:
88 | $this->_processUpdate($svc);
89 | $msg = __('MCP service configuration has been successfully updated!', 'convoworks-gpt');
90 | $type = 'success';
91 | break;
92 | default:
93 | throw new \Exception('Unexpected action [' . $this->getAction() . ']');
94 | }
95 | add_settings_error('convo_mcp_settings', 'convo_mcp_settings', $msg, $type);
96 | } catch (\Exception $e) {
97 | /** @phpstan-ignore-next-line */
98 | $this->_logger->error($e);
99 | add_settings_error('convo_mcp_settings', 'convo_mcp_settings', $e->getMessage(), 'error');
100 | }
101 |
102 | set_transient('convo_mcp_settings_errors', get_settings_errors('convo_mcp_settings'), 30);
103 | wp_safe_redirect($this->_viewModel->getBaseUrl($svc));
104 | exit;
105 | }
106 |
107 | private function _processCreate($serviceId)
108 | {
109 | $this->_logger->info('Creating mcp service for [' . $serviceId . ']');
110 | $basicAuth = isset($_POST['basic_auth']) && sanitize_text_field($_POST['basic_auth']) === '1';
111 | $this->_mcpManager->enableMcp($serviceId, $basicAuth);
112 | }
113 |
114 | private function _processDisable($serviceId)
115 | {
116 | // update amazon config
117 | $this->_logger->info('Disabling mcp service for [' . $serviceId . ']');
118 | $this->_mcpManager->disableMcp($serviceId);
119 | }
120 |
121 | private function _processUpdate($serviceId)
122 | {
123 | $this->_logger->info('Updating mcp service for [' . $serviceId . ']');
124 | $basicAuth = isset($_POST['basic_auth']) && sanitize_text_field($_POST['basic_auth']) === '1';
125 | $this->_mcpManager->updateMcp($serviceId, $basicAuth);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
1 | # Convoworks GPT - Cursor Rules
2 |
3 | Always include the following documentation files in chat context when working on this project:
4 |
5 | - AGENTS.md (project overview and navigation)
6 | - README.md (user-facing documentation, build instructions, features)
7 | - CHANGELOG.md (version history and recent changes)
8 |
9 | When relevant to PHP framework work, also reference documentation from the parent Convoworks WP plugin:
10 | - ../convoworks-wp/AGENTS.md (overview)
11 | - ../convoworks-wp/src/AGENTS_CONVOWORKS_FWRK.md (PHP framework guide)
12 |
13 | ## Project-Specific Guidelines
14 |
15 | ### PHP Development
16 |
17 | - This plugin extends Convoworks WP – always verify the parent plugin is available
18 | - Main namespace is `Convo\Gpt` – all plugin code lives in `src/Convo/Gpt/`
19 | - Follow Convoworks framework conventions (see parent plugin docs)
20 | - Components must implement appropriate workflow interfaces from `Convo\Core\Workflow`
21 | - Use dependency injection – services are wired in Convoworks WP's DI container
22 |
23 | ### Component Help Files
24 |
25 | - Prefer Markdown (`.md`) for all new help files
26 | - Place help files in `src/Convo/Gpt/Pckg/Help/`
27 | - Follow naming convention: `component-name.html` or `component-name.md`
28 | - Reference Convoworks WP's component help guidelines for structure
29 |
30 | ### Version Management
31 |
32 | - Version is managed in `package.json` (single source of truth)
33 | - Run `npm run sync-version` after version changes to update `composer.json` and `convoworks-gpt.php`
34 | - Update `CHANGELOG.md` for all user-facing changes
35 | - Follow semantic versioning (major.minor.patch)
36 |
37 | ### Build & Distribution
38 |
39 | - No frontend bundles – this plugin has no JavaScript UI
40 | - **Local development**: No build needed, just `composer install` and activate
41 | - **Release builds**: Automated via GitHub Actions when tags are pushed
42 | - **Manual build** (if needed): `npm run build` creates distributable zip
43 | - Test built packages in a clean WordPress install with Convoworks WP
44 | - **GitHub Actions**: Push a tag (`git tag v0.16.2 && git push --tags`) to trigger automated release
45 |
46 | ### MCP Server Development
47 |
48 | - MCP-related code lives in `src/Convo/Gpt/Mcp/`
49 | - Session storage is currently filesystem-based (may migrate to DB later)
50 | - Streamable HTTP implementation follows MCP 2024-11-05 spec
51 | - WordPress REST API tools are registered via filters for lazy loading
52 |
53 | ### Testing & Code Quality
54 |
55 | - **PHPStan**: Run `vendor/bin/phpstan analyse` before committing to catch type errors
56 | - Level 5 configured in `phpstan.neon`
57 | - Ignores WordPress symbols (they're not available during analysis)
58 | - If too many errors, consider generating a baseline: `vendor/bin/phpstan analyse --generate-baseline`
59 | - **PHPUnit**: Run `vendor/bin/phpunit` to execute unit tests
60 | - Tests in `tests/` focus on utility functions (serialization, truncation, etc.)
61 | - Add tests for new utility functions
62 | - **Manual testing**: Use one of the provided service templates
63 | - Always test with Convoworks WP activated first
64 | - Test MCP endpoints with compatible clients (cLine, Claude Desktop with proxy, etc.)
65 |
66 | ### Code Style & Conventions
67 |
68 | - Follow PSR-4 autoloading (already configured in composer.json)
69 | - Use type hints where possible (PHP 7.2+ compatible)
70 | - Document public methods with PHPDoc blocks
71 | - Keep component classes focused – one responsibility per class
72 | - Use descriptive variable names (no single-letter vars except in loops)
73 |
74 | ## Common Pitfalls
75 |
76 | - Don't forget to register new components in `GptPackageDefinition::_initDefintions()`
77 | - Don't modify Convoworks core classes – extend and compose instead
78 | - Don't add frontend build steps – this plugin has no UI bundle
79 | - Don't bundle heavy PHP dependencies – keep the plugin lightweight
80 | - Don't forget to update help files when adding/changing component properties
81 |
82 | ## Quick Reference
83 |
84 | ### Key Files
85 |
86 | - `convoworks-gpt.php` – Plugin entry point, constants, WordPress hooks
87 | - `src/Convo/Gpt/GptPlugin.php` – Main plugin registration
88 | - `src/Convo/Gpt/Pckg/GptPackageDefinition.php` – Component package definition
89 | - `src/Convo/Gpt/Mcp/McpServerPlatform.php` – MCP platform adapter
90 | - `build.js` – Build script (creates distributable zip)
91 | - `sync-version.js` – Version synchronization script
92 |
93 | ### Common npm Commands
94 |
95 | - `npm install` – Install packaging dependencies
96 | - `npm run build` – Create distributable zip (usually done by GitHub Actions)
97 | - `npm run sync-version` – Sync version from package.json to other files
98 | - `npm run clean` – Remove all build artifacts
99 |
100 | ### Common Composer Commands
101 |
102 | - `composer install` – Install dev dependencies (for local testing)
103 | - `composer update` – Update dependencies
104 | - `vendor/bin/phpunit` – Run unit tests
105 | - `vendor/bin/phpstan analyse` – Run static analysis
106 |
107 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/conversation-messages-element.md:
--------------------------------------------------------------------------------
1 | ### Conversation Messages
2 |
3 | Manages and provides the entire conversation history to the **GPT Chat Completion API v2**, ensuring consistent context throughout the interaction. This element loads a stored conversation array and registers each message with the parent Chat Completion component.
4 |
5 | ### When to use
6 |
7 | Use **Conversation Messages** when you need to:
8 |
9 | - Load a conversation history stored in session or database
10 | - Provide the full message thread to GPT Chat Completion API v2
11 | - Maintain conversation context across multiple turns
12 | - Integrate with **GPT Chat Completion API v2**'s message provider flow
13 |
14 | This element must be used inside the **message_provider** flow of **GPT Chat Completion API v2**.
15 |
16 | ### Properties
17 |
18 | #### Messages
19 |
20 | Expression that evaluates to an array of conversation messages. Each message should be an object with `role` and `content` properties following the OpenAI chat format.
21 |
22 | Example values:
23 |
24 | - `${conversation}` – Load from session variable
25 | - `${get_conversation(user_id)}` – Load from custom function
26 | - `${[]}` – Start with empty conversation
27 |
28 | **Message format**:
29 |
30 | ```text
31 | ${[
32 | {"role": "system", "content": "You are a helpful assistant."},
33 | {"role": "user", "content": "Hello!"},
34 | {"role": "assistant", "content": "Hi! How can I help you?"}
35 | ]}
36 | ```
37 |
38 | ### Runtime behavior
39 |
40 | When the element executes:
41 |
42 | 1. The messages expression is evaluated
43 | 2. If the result is an array, each message is registered with the parent `IMessages` container (typically **GPT Chat Completion API v2**)
44 | 3. The registered messages become part of the conversation context sent to the API
45 |
46 | **Important**: This element does not modify or store messages – it only loads and registers them. Message persistence is handled separately (typically in the **New Message Flow** of Chat Completion v2).
47 |
48 | ### Integration with Chat Completion API v2
49 |
50 | **Conversation Messages** is typically used inside the **Messages** (message_provider) flow of **GPT Chat Completion API v2**:
51 |
52 | **Chat Completion v2 configuration**:
53 |
54 | - **Messages flow**:
55 | 1. **System Message**: `You are a helpful assistant for ${site_name}.`
56 | 2. **Conversation Messages**: `${conversation}` ← Loads conversation history
57 | 3. *[Optional]* **Messages Limiter** or **Simple Messages Limiter** to manage context length
58 |
59 | - **New Message Flow**:
60 | 1. **Set Param** (session scope):
61 | - Name: `conversation`
62 | - Value: `${array_merge(conversation, [status.last_message])}` ← Saves new messages
63 |
64 | This pattern ensures:
65 | - Messages are loaded before each API call
66 | - New messages (both from GPT and function calls) are appended to conversation
67 | - Full conversation history is maintained across turns
68 |
69 | ### Example
70 |
71 | **Basic conversation management**:
72 |
73 | **Service Variables** (Configuration → Variables):
74 |
75 | - `conversation` (default: `${[]}`)
76 |
77 | **Chat Completion v2 configuration**:
78 |
79 | **Messages flow**:
80 |
81 | 1. **System Message**:
82 | - Content: `You are a friendly customer support assistant. Today is ${date("l, F j, Y")}.`
83 |
84 | 2. **Conversation Messages**:
85 | - Messages: `${conversation}`
86 |
87 | 3. **Simple Messages Limiter**:
88 | - Max Tokens: `${8000}`
89 | - Truncate to Tokens: `${4000}`
90 | - Messages: (nested) **Conversation Messages** with `${conversation}`
91 |
92 | **New Message Flow**:
93 |
94 | 1. **Set Param**:
95 | - Scope: `session`
96 | - Name: `conversation`
97 | - Value: `${array_merge(conversation, [status.last_message])}`
98 |
99 | **OK Flow**:
100 |
101 | 1. **Text Response**: `${status.last_message.content}`
102 |
103 | ### Example: Database-backed conversation
104 |
105 | **Load from custom storage**:
106 |
107 | **Messages flow**:
108 |
109 | 1. **PHP Delegate**:
110 | - Code: Load conversation from database
111 | - Result variable: `db_conversation`
112 |
113 | 2. **Conversation Messages**:
114 | - Messages: `${db_conversation}`
115 |
116 | **New Message Flow**:
117 |
118 | 1. **PHP Delegate**:
119 | - Code: Save new message to database
120 | - Parameters: `${status.last_message}`
121 |
122 | ### Tips
123 |
124 | - Always initialize the conversation variable with an empty array `${[]}` in service configuration
125 | - Use session scope for the conversation variable to persist across requests
126 | - Wrap **Conversation Messages** inside a **Messages Limiter** or **Simple Messages Limiter** to prevent context length overflow
127 | - The messages array can come from session variables, database queries, API responses, or any expression that evaluates to an array
128 | - Each message object should have at minimum `role` and `content` properties
129 | - Optionally, messages can include `name` (for function calls), `tool_calls`, `tool_call_id`, or `transient` flags
130 | - If the expression evaluates to null or non-array, no messages are registered (no error)
131 | - Don't confuse with **System Message** – use **Conversation Messages** for loading multi-turn history, use **System Message** for single static/dynamic instructions
132 | - For debugging, log the messages count: `${count(conversation)}` to monitor conversation growth
133 |
134 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/embeddings-element.md:
--------------------------------------------------------------------------------
1 | ### GPT Embeddings API
2 |
3 | Wrapper for the OpenAI Embeddings API, enabling you to generate vector representations (embeddings) for text input. Embeddings are useful for semantic search, clustering, recommendations, and similarity comparisons.
4 |
5 | ### When to use
6 |
7 | Use **GPT Embeddings API** when you need to:
8 |
9 | - Create vector embeddings for semantic search in your content
10 | - Build recommendation systems based on text similarity
11 | - Cluster or categorize text content automatically
12 | - Compare semantic similarity between documents
13 | - Store vector representations for RAG (Retrieval-Augmented Generation) systems
14 |
15 | ### Properties
16 |
17 | #### Input
18 |
19 | The text input for which you want to create an embedding. Can be a string or an array of strings (depending on the model).
20 |
21 | **Preprocessing recommendation**: Clean up raw text using the `tokenize_string()` function to remove stop words and improve embedding quality:
22 |
23 | ```text
24 | ${tokenize_string(your_raw_text)}
25 | ```
26 |
27 | Example values:
28 |
29 | - `${page_content}` – Content from a WordPress page
30 | - `${tokenize_string(post.content)}` – Tokenized post content
31 | - `The quick brown fox jumps over the lazy dog` – Literal string
32 |
33 | #### API key
34 |
35 | Your OpenAI API key. Store this in a service variable for security.
36 |
37 | Example: `${GPT_API_KEY}`
38 |
39 | #### Base URL
40 |
41 | Optional. Base URL for the API endpoint. If left blank, defaults to `https://api.openai.com/v1`.
42 |
43 | Use this to point to custom endpoints or OpenAI-compatible APIs.
44 |
45 | #### API options
46 |
47 | Configuration options for the Embeddings API:
48 |
49 | - **model** – The embedding model to use (e.g., `text-embedding-ada-002`, `text-embedding-3-small`, `text-embedding-3-large`)
50 |
51 | Default value:
52 |
53 | ```text
54 | model: text-embedding-ada-002
55 | ```
56 |
57 | For all available options and models, see the [OpenAI Embeddings API Reference](https://platform.openai.com/docs/api-reference/embeddings).
58 |
59 | #### Result Variable Name
60 |
61 | The name of the variable that will store the complete embeddings API response. Default is `status`.
62 |
63 | #### OK flow
64 |
65 | Sub-flow executed after the API call completes successfully. The result variable will be available for use in this flow.
66 |
67 | ### Runtime behavior
68 |
69 | When the element executes:
70 |
71 | 1. The input text is evaluated
72 | 2. An API call is made to OpenAI Embeddings endpoint with the input and options
73 | 3. The complete response is stored in the result variable (request scope)
74 | 4. The OK flow is executed with the result variable available
75 |
76 | ### Accessing the embedding
77 |
78 | Inside the OK flow, access the embedding vector like this:
79 |
80 | ```text
81 | ${status.data[0].embedding}
82 | ```
83 |
84 | The result variable contains:
85 |
86 | - `${status.data}` – Array of embedding objects
87 | - `${status.data[0].embedding}` – The actual embedding vector (array of floats)
88 | - `${status.data[0].index}` – Index of the embedding in the input array
89 | - `${status.usage}` – Token usage statistics
90 |
91 | The embedding vector is a numerical array (typically 1536 dimensions for `text-embedding-ada-002`, or configurable dimensions for newer models).
92 |
93 | ### Example
94 |
95 | **Building a semantic search index**:
96 |
97 | **Configuration**:
98 |
99 | - **Input**: `${tokenize_string(post.post_content)}`
100 | - **API key**: `${GPT_API_KEY}`
101 | - **API options**:
102 | - model: `text-embedding-3-small`
103 | - **Result Variable Name**: `embedding_result`
104 |
105 | **In the OK flow**, store the embedding:
106 |
107 | 1. Add **HTTP Request** to store in your vector database:
108 | - URL: `https://your-vector-db.com/api/embeddings`
109 | - Method: `POST`
110 | - Body: `${{"post_id": post.ID, "embedding": embedding_result.data[0].embedding}}`
111 |
112 | Or store in WordPress post meta:
113 |
114 | 1. Add **Set Meta** element:
115 | - Post ID: `${post.ID}`
116 | - Meta key: `_embedding_vector`
117 | - Meta value: `${json_encode(embedding_result.data[0].embedding)}`
118 |
119 | ### Example: Semantic similarity search
120 |
121 | **Find similar posts**:
122 |
123 | 1. Get embedding for search query using **GPT Embeddings API**
124 | 2. In OK flow, compare with stored embeddings using cosine similarity
125 | 3. Rank posts by similarity score
126 |
127 | ### Tips
128 |
129 | - Use `tokenize_string()` to preprocess text – removes stop words and improves embedding quality
130 | - For long documents, split into chunks and embed each chunk separately (embeddings have token limits)
131 | - Use `text-embedding-3-small` for cost-effective embeddings with good quality
132 | - Use `text-embedding-3-large` for highest quality when accuracy is critical
133 | - Store embeddings in a vector database (Pinecone, Weaviate, pgvector) for production semantic search
134 | - For WordPress-based vector search, store embeddings in post meta or a custom table
135 | - Calculate cosine similarity between embeddings to measure semantic similarity (dot product if vectors are normalized)
136 | - Batch multiple inputs in a single API call when possible to reduce costs
137 | - Monitor token usage through `${status.usage.total_tokens}` – embeddings are charged per token
138 | - Cache embeddings – they don't change unless the input text changes
139 |
140 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## Convoworks GPT WordPress plugin
3 |
4 | ### 0.16.1 - 2025-12-04
5 |
6 | * Added the ability to disable expression language evaluation for system prompts.
7 | * Updated the method for accessing the Convoworks WP plugin.
8 |
9 | ### 0.16.0 - 2025-08-23 - MCP 2025-06-18
10 |
11 | * Updated to MCP specification 2025-06-18 and Streamable HTTP.
12 | * Added support for basic authentication in MCP server, using WordPress application passwords.
13 |
14 | ### 0.15.0 - 2025-06-22 - Token-based limiters
15 |
16 | * Message limiter elements now use token-based limits.
17 | * You can now limit function call result size (in tokens).
18 | * Removed old, deprecated components.
19 | * Added `estimate_tokens(content)` function.
20 |
21 | ### 0.14.0 - 2025-04-25 - Session ping
22 |
23 | * Improved keep alive mechanism and check
24 | * Added missing MCP logo
25 |
26 | ### 0.13.2 - 2025-04-23 - Function scope fix
27 |
28 | * Initialize properly scoped function call
29 |
30 | ### 0.13.1 - 2025-04-22 - Menu fix
31 |
32 | * Remove MCP settings page from menu
33 |
34 | ### 0.13.0 - 2025-04-22 - MCP Server Support
35 |
36 | * Added MCP Server platform.
37 | * Added **MCP Server Example** service template.
38 | * Added MCP Processor for routing MCP requests.
39 | * Added WP REST Proxy Function for forwarding REST API calls.
40 | * Added Simple MCP Prompt Template.
41 |
42 |
43 | ### 0.12.2 - 2025-03-27 - Deep Research template
44 |
45 | * Added new service template - Deep Research Assistant.
46 |
47 |
48 | ### 0.12.1 - 2025-02-23 - Chat Functions Handling
49 |
50 | * Improved chat function execution with better error handling—now catches `Throwable`.
51 | * Used scoped functions as the base for all chat functions, enabling local function scope.
52 |
53 |
54 | ### 0.12.0 Preview fixes
55 |
56 | * Added function scope support to the Chat Function element
57 |
58 | ### 0.11.2 Preview fixes
59 |
60 | * Fixed not displayed help for the Conversation Messages Element.
61 | * Long System GPT messages are now trimmed (12 lines) when displayed in editor.
62 | * The Chat Function Element now has description displayed in editor.
63 | * Fixed message serialization when message content is null (tool call).
64 |
65 | ### 0.11.1 Truncate messages
66 |
67 | * Fixed the truncate messages function - grouping
68 |
69 | ### 0.11.0 Truncate messages
70 |
71 | * Fixed truncate messages functions to preserve message grouping
72 | * Message limiter elements now have truncated flow
73 |
74 | ### 0.10.0 Catchup with OpenAI API changes - tools
75 |
76 | * GPT API components now support base url parameter, enabling using other AI providers
77 | * Chat compčletion element now uses functions as part of tools definition
78 | * Added `unserialize_gpt_messages()` function for unserializing stringified conversation into associative array.
79 |
80 | ### 0.9.2 Updated Service Templates
81 |
82 | - Updated both service templates to take advantage of new features.
83 | - Revised documentation and help files.
84 |
85 | ### 0.9.1 Serialize messages function
86 |
87 | * Added `serialize_gpt_messages()` function for serialisation messages into readable string.
88 |
89 | ### 0.9.0 External gpt functions element
90 |
91 | * Added `ExternalChatFunctionElement` which allows registering GPT functions from 3rd party plugins
92 |
93 | ### 0.8.0
94 |
95 | * Added simple messages limiter
96 | * Chat completion now rebuilds context even adter function calls
97 | * Chat completion now has on a new message flow to more transparent messages handling
98 | * Added `split_text_into_chunks()` el function
99 | * Added `SystemMessageGroupElement` for grouping prompt parts into the single system message.
100 |
101 | ### 0.7.3 Regenerate context
102 |
103 | * In function call scenarios, prompt (and functions) are now rebuilt for each GPT API call.
104 |
105 | ### 0.7.2 JSON schema fix
106 |
107 | * Fixed `args` argument definition for the `call_user_func_array` in `gpt-site-assistant.template.json`
108 |
109 | ### 0.7.1 Arry syntax fix
110 |
111 | * Corrected extra comma issue in `ChatCompletionV2Element` which caused error on the PHP 7.2
112 |
113 | ### 0.7.0 Include Function Results in Summarization
114 |
115 | * Added the ability to include function results in the summarization process.
116 | * Introduced `${conversation}`, a conversation summary that can be utilized within the query generator prompt.
117 |
118 | ### 0.6.0 Improved Chat Functions Execution
119 |
120 | * Enhanced the execution of chat functions: Included fixes for JSON parsing, handled endless loops better, and improved error handling.
121 | * Optimized the trimming of conversation messages.
122 |
123 | ### 0.5.0 Chat functions support
124 |
125 | * New Chat GPT API component with support for functions
126 | * Added System Message and Message Limiter elements
127 | * Added Embeddings and Moderation API elements
128 |
129 | ### 0.4.0 Auto activated actions
130 |
131 | * Add ability to mark actions as auto activated (e.g. logged user info)
132 | * Add ability to use just main Chat app prompt - child prompts will be ignored. Useful when testing and tuning prompts.
133 |
134 | ### 0.3.0 Turbo Chat App
135 |
136 | * Add chat app which uses GPT-3.5-turbo API
137 | * Update service template with the Turbochat app example
138 |
139 | ### 0.2.0 Refactor Chat App
140 |
141 | * Refactor prompt & action interfaces
142 | * Add validation error element
143 | * Remove actions prompt element
144 | * Update service template with appointment scheduling
145 |
146 | ### 0.1.0 Initial version
147 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/chat-completion-element.md:
--------------------------------------------------------------------------------
1 | ### GPT Chat Completion API
2 |
3 | Allows you to execute OpenAI Chat Completion API calls and get chat completion responses. This is a simple wrapper element for making chat API calls with a system message and conversation history.
4 |
5 | ### When to use
6 |
7 | Use **GPT Chat Completion API** when you need to:
8 |
9 | - Make straightforward chat completion API calls with OpenAI
10 | - Quickly test GPT responses with a simple system prompt
11 | - Build basic conversational interfaces without function calling
12 | - Get chat completions without complex message management
13 |
14 | For more advanced scenarios with function calling, dynamic message providers, and better control over the conversation flow, consider using **GPT Chat Completion API v2** instead.
15 |
16 | ### Properties
17 |
18 | #### System message
19 |
20 | The main system prompt that will be automatically prepended to the conversation. This sets the behavior and context for the AI assistant.
21 |
22 | Example values:
23 |
24 | - `The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. Today is ${date("l, F j, Y")}.`
25 | - `You are a customer support assistant for our e-commerce platform. Be professional and helpful.`
26 |
27 | The system message is evaluated as an expression, so you can use dynamic values and functions.
28 |
29 | #### Messages
30 |
31 | An array of message objects in the OpenAI chat format. Each message should have a `role` (user, system, or assistant) and `content` (the message text).
32 |
33 | Example format:
34 |
35 | ```text
36 | ${[
37 | {"role": "user", "content": "Hello!"},
38 | {"role": "assistant", "content": "Hi! How can I help you today?"},
39 | {"role": "user", "content": "Tell me a joke."}
40 | ]}
41 | ```
42 |
43 | **Important**: This element does not automatically manage message persistence through the session. You are responsible for storing and retrieving the conversation history from session variables.
44 |
45 | #### API key
46 |
47 | Your OpenAI API key. This should typically be stored in a service variable for security.
48 |
49 | Example: `${GPT_API_KEY}`
50 |
51 | #### Base URL
52 |
53 | Optional. Base URL for the API endpoint. If left blank, the default OpenAI API endpoint is used: `https://api.openai.com/v1`
54 |
55 | Use this to point to custom endpoints, proxy servers, or OpenAI-compatible APIs.
56 |
57 | #### API options
58 |
59 | Configuration parameters for the Chat Completion API. Common options include:
60 |
61 | - **model** – The model to use (e.g., `gpt-4o`, `gpt-4`, `gpt-3.5-turbo`)
62 | - **temperature** – Controls randomness (0.0 to 2.0). Lower values make output more focused and deterministic
63 | - **max_tokens** – Maximum number of tokens in the response
64 |
65 | Default values:
66 |
67 | ```text
68 | model: gpt-4o
69 | temperature: ${0.7}
70 | max_tokens: ${4096}
71 | ```
72 |
73 | For all available options, see the [OpenAI Chat Completion API Reference](https://platform.openai.com/docs/api-reference/chat).
74 |
75 | #### Result Variable Name
76 |
77 | The name of the variable that will store the complete Chat Completion API response. Default is `status`.
78 |
79 | The response contains the full API response object, including choices, usage statistics, and metadata.
80 |
81 | #### OK flow
82 |
83 | Sub-flow executed after the API call completes successfully. The result variable will be available for use in this flow.
84 |
85 | ### Runtime behavior
86 |
87 | When the element executes:
88 |
89 | 1. The system message is evaluated and prepended to the messages array
90 | 2. An API call is made to OpenAI with the combined messages and options
91 | 3. The complete response is stored in the result variable (request scope)
92 | 4. The OK flow is executed with the result variable available
93 |
94 | ### Accessing the response
95 |
96 | Inside the OK flow, access the completion text like this:
97 |
98 | ```text
99 | ${status.choices[0]["message"]["content"]}
100 | ```
101 |
102 | The result variable contains the complete API response structure:
103 |
104 | - `${status.choices}` – Array of completion choices
105 | - `${status.choices[0].message}` – The assistant's message object
106 | - `${status.choices[0].message.content}` – The actual response text
107 | - `${status.usage}` – Token usage statistics
108 |
109 | ### Example
110 |
111 | **Configuration**:
112 |
113 | - **System message**: `You are a helpful travel assistant. Provide concise information about destinations.`
114 | - **Messages**: `${conversation}`
115 | - **API key**: `${GPT_API_KEY}`
116 | - **API options**:
117 | - model: `gpt-4o`
118 | - temperature: `${0.5}`
119 | - max_tokens: `${1000}`
120 | - **Result Variable Name**: `gpt_response`
121 |
122 | **In the OK flow**, add a Text Response element:
123 |
124 | > ${gpt_response.choices[0]["message"]["content"]}
125 |
126 | ### Tips
127 |
128 | - Store your API key in a service variable (Configuration → Variables) instead of hardcoding it
129 | - Use lower temperature values (0.1-0.3) for more consistent, focused responses
130 | - Use higher temperature values (0.7-1.0) for more creative, varied responses
131 | - Always handle the response inside the OK flow to ensure the API call completed successfully
132 | - For production use with ongoing conversations, store the messages array in session scope and append to it with each turn
133 | - Consider using **GPT Chat Completion API v2** if you need function calling or more flexible message management
134 | - Monitor your token usage through `${status.usage.total_tokens}` to optimize costs
135 |
136 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Admin/McpConvoworksManager.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
39 | $this->_package = $package;
40 | $this->_convoServiceDataProvider = $convoServiceDataProvider;
41 | }
42 |
43 | public function isServiceEnabled($serviceId)
44 | {
45 | $user = new AdminUser(wp_get_current_user());
46 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig(
47 | $user,
48 | $serviceId,
49 | IPlatformPublisher::MAPPING_TYPE_DEVELOP
50 | );
51 | return isset($config[McpServerPlatform::PLATFORM_ID]);
52 | }
53 |
54 | public function getServiceName($serviceId)
55 | {
56 | $user = new AdminUser(wp_get_current_user());
57 | $service = $this->_convoServiceDataProvider->getServiceData(
58 | $user,
59 | $serviceId,
60 | IPlatformPublisher::MAPPING_TYPE_DEVELOP
61 | );
62 | return $service['name'];
63 | }
64 |
65 | public function enableMcp($serviceId, $basicAuth = false)
66 | {
67 | $this->_logger->info('Enabling service [' . $serviceId . ']');
68 |
69 | $user = new AdminUser(wp_get_current_user());
70 | $publisher = $this->_getMcpServerPublisher($user, $serviceId);
71 |
72 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig(
73 | $user,
74 | $serviceId,
75 | IPlatformPublisher::MAPPING_TYPE_DEVELOP
76 | );
77 |
78 | $config[McpServerPlatform::PLATFORM_ID] = [];
79 | $config[McpServerPlatform::PLATFORM_ID]['time_created'] = time();
80 | $config[McpServerPlatform::PLATFORM_ID]['time_updated'] = time();
81 | $config[McpServerPlatform::PLATFORM_ID]['basic_auth'] = $basicAuth;
82 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config);
83 | $publisher->enable();
84 | }
85 |
86 | public function updateMcp($serviceId, $basicAuth = false)
87 | {
88 | $this->_logger->info('Updating MCP config for service [' . $serviceId . ']');
89 | $user = new AdminUser(wp_get_current_user());
90 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig(
91 | $user,
92 | $serviceId,
93 | IPlatformPublisher::MAPPING_TYPE_DEVELOP
94 | );
95 | $platformId = McpServerPlatform::PLATFORM_ID;
96 | if (!isset($config[$platformId])) {
97 | throw new NotFoundException('Service [' . $serviceId . '] config [' . $platformId . '] not found');
98 | }
99 | $config[$platformId]['basic_auth'] = $basicAuth;
100 | $config[$platformId]['time_updated'] = time();
101 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config);
102 | }
103 |
104 | public function disableMcp($serviceId)
105 | {
106 | $platformId = McpServerPlatform::PLATFORM_ID;
107 | $user = new AdminUser(wp_get_current_user());
108 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig(
109 | $user,
110 | $serviceId,
111 | IPlatformPublisher::MAPPING_TYPE_DEVELOP
112 | );
113 |
114 | if (!isset($config[$platformId])) {
115 | throw new NotFoundException('Service [' . $serviceId . '] config [' . $platformId . '] not found');
116 | }
117 |
118 | // RELEASES ?
119 | // PUBLISHER
120 | $publisher = $this->_getMcpServerPublisher($user, $serviceId);
121 | $report = [];
122 | $publisher->delete($report);
123 | $this->_logger->info('Platform delete report [' . print_r($report, true) . ']');
124 |
125 | // CONFIG
126 | $this->_logger->info('Deleting configuration [' . $platformId . '] for service [' . $serviceId . ']');
127 | unset($config[$platformId]);
128 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config);
129 | }
130 |
131 | /**
132 | * @param IAdminUser $user
133 | * @param string $serviceId
134 | * @return IPlatformPublisher
135 | */
136 | private function _getMcpServerPublisher($user, $serviceId)
137 | {
138 | /** @var IPlatformProvider $package */
139 | $package = $this->_package->getPackageInstance();
140 | $platform = $package->getPlatform(McpServerPlatform::PLATFORM_ID);
141 | return $platform->getPlatformPublisher($user, $serviceId);
142 | }
143 |
144 | // UTIL
145 | public function __toString()
146 | {
147 | return get_class($this);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/group-system-messages-element.md:
--------------------------------------------------------------------------------
1 | ### Group System Messages
2 |
3 | Groups multiple child **System Message** elements into a single combined system message. This helps organize complex prompts and manage multiple system instructions more effectively.
4 |
5 | ### When to use
6 |
7 | Use **Group System Messages** when you need to:
8 |
9 | - Combine multiple system messages into one
10 | - Organize complex prompts with conditional or dynamic parts
11 | - Reduce the number of separate system messages in the conversation
12 | - Build system prompts from modular components
13 | - Control formatting of combined system instructions
14 |
15 | This element is typically used in the **Messages** (message_provider) flow of **GPT Chat Completion API v2**.
16 |
17 | ### Properties
18 |
19 | #### Trim Child Prompts (trim_children)
20 |
21 | Controls how child messages are joined together:
22 |
23 | - **false** (default): Child messages are separated with double newlines (`\n\n`)
24 | - **true**: Child messages are joined inline without spacing (trimmed)
25 |
26 | Use `true` for compact, inline formatting. Use `false` for better readability with paragraph breaks.
27 |
28 | #### Messages (message_provider)
29 |
30 | A sub-flow containing **System Message** elements that will be grouped. The sub-flow executes, collecting all child system messages, then combines them into a single system message.
31 |
32 | ### Runtime behavior
33 |
34 | When the element executes:
35 |
36 | 1. The message_provider sub-flow runs, collecting all **System Message** elements
37 | 2. The content of each child message is extracted
38 | 3. If `trim_children` is true, contents are joined inline (no spacing)
39 | 4. If `trim_children` is false, contents are separated with `\n\n`
40 | 5. A single combined system message is registered with the parent `IMessages` container
41 | 6. The combined message is marked as `transient: true`
42 |
43 | ### Message structure
44 |
45 | The registered message has this structure:
46 |
47 | ```text
48 | {
49 | "role": "system",
50 | "transient": true,
51 | "content": ""
52 | }
53 | ```
54 |
55 | ### Example
56 |
57 | **Grouping role and instructions**:
58 |
59 | **Group System Messages**:
60 |
61 | - **Trim Child Prompts**: `false`
62 | - **Messages** sub-flow:
63 | 1. **System Message**: `You are a helpful WordPress site administrator assistant.`
64 | 2. **System Message**: `You have access to the WordPress REST API for managing posts, pages, users, and settings.`
65 | 3. **System Message**: `Always confirm before making destructive changes like deleting content or deactivating plugins.`
66 |
67 | **Result**: A single system message with content:
68 |
69 | ```text
70 | You are a helpful WordPress site administrator assistant.
71 |
72 | You have access to the WordPress REST API for managing posts, pages, users, and settings.
73 |
74 | Always confirm before making destructive changes like deleting content or deactivating plugins.
75 | ```
76 |
77 | ### Example: Inline formatting
78 |
79 | **Group System Messages**:
80 |
81 | - **Trim Child Prompts**: `true`
82 | - **Messages** sub-flow:
83 | 1. **System Message**: `Current date: ${date("Y-m-d")}.`
84 | 2. **System Message**: ` User: ${current_user.display_name}.`
85 | 3. **System Message**: ` Site: ${site_name}.`
86 |
87 | **Result**: A single compact system message:
88 |
89 | ```text
90 | Current date: 2025-12-04. User: John Doe. Site: My WordPress Site.
91 | ```
92 |
93 | ### Example: Conditional system instructions
94 |
95 | **Group System Messages**:
96 |
97 | - **Trim Child Prompts**: `false`
98 | - **Messages** sub-flow:
99 | 1. **System Message**: `You are a customer support assistant for ${company_name}.`
100 | 2. **If** element: `${user_is_premium}`
101 | - **True flow**: **System Message**: `This is a premium customer. Provide priority support and offer advanced features.`
102 | 3. **If** element: `${hour(date("H")) >= 18}`
103 | - **True flow**: **System Message**: `It's currently after business hours. Inform the user that live chat support resumes at 9 AM.`
104 |
105 | **Result**: A dynamically composed system message based on user status and time.
106 |
107 | ### Example: Modular prompt components
108 |
109 | **Organize complex prompts into logical sections**:
110 |
111 | **Group System Messages**:
112 |
113 | - **Messages** sub-flow:
114 | 1. **Group System Messages** (Role definition):
115 | - **System Message**: `You are an AI assistant specialized in technical support.`
116 | 2. **Group System Messages** (Available tools):
117 | - **System Message**: `You have access to the following tools: search_knowledge_base, create_support_ticket, check_server_status.`
118 | 3. **Group System Messages** (Rules and guidelines):
119 | - **System Message**: `Never share sensitive information. Always verify user identity before accessing account details.`
120 |
121 | ### Tips
122 |
123 | - Use `trim_children: false` for better readability – it's easier for GPT to parse separate paragraphs
124 | - Use `trim_children: true` when building compact inline metadata or lists
125 | - Group related instructions together – don't mix unrelated concepts in one group
126 | - Combine with conditional elements (**If**) to dynamically include/exclude sections based on context
127 | - Nest **Group System Messages** elements to create hierarchical prompt structures
128 | - Use **Group System Messages** to separate static prompts from dynamic ones
129 | - Remember that the grouped message still counts as a single system message in the API request
130 | - Grouped messages are marked `transient: true` – they won't appear in `getConversation()` results
131 | - This element is particularly useful when building prompts from template files or database-stored instructions
132 | - For very long prompts, consider using **Messages Limiter** to keep total context under control
133 |
134 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/WpRestProxyFunction.php:
--------------------------------------------------------------------------------
1 | _name = $properties['name'];
28 | $this->_description = $properties['description'];
29 | $this->_parameters = $properties['parameters'];
30 | $this->_defaults = $properties['defaults'];
31 | $this->_required = $properties['required'];
32 | $this->_method = $properties['method'];
33 | $this->_endpoint = $properties['endpoint'];
34 | $this->_pagination = $properties['pagination'];
35 | }
36 |
37 |
38 | // element
39 | public function read(IConvoRequest $request, IConvoResponse $response)
40 | {
41 | /** @var \Convo\Gpt\IChatFunctionContainer $container */
42 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer');
43 | $container->registerFunction($this);
44 | }
45 |
46 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data)
47 | {
48 | $this->_logger->debug('Got data decoded [' . $this->getName() . '] - [' . print_r($data, true) . ']');
49 | $data = array_merge($this->_getDefaults(), $data);
50 | $this->_logger->info('Got data with defaults [' . $this->getName() . '] - [' . print_r($data, true) . ']');
51 |
52 | $method = strtoupper($this->evaluateString($this->_method));
53 | $endpoint = ltrim($this->evaluateString($this->_endpoint), '/');
54 | $pagination = $this->evaluateString($this->_pagination);
55 |
56 | // Handle pagination via cursor
57 | if ($pagination && isset($data['cursor'])) {
58 | $cursorData = json_decode(base64_decode($data['cursor']), true);
59 | if (is_array($cursorData)) {
60 | unset($data['cursor']);
61 | $data = array_merge($data, $cursorData);
62 | }
63 | }
64 |
65 | $route = "/wp/v2/{$endpoint}";
66 | $this->_logger->debug('Executing [' . $method . '] - [' . $route . ']');
67 | $restRequest = new \WP_REST_Request($method, $route);
68 |
69 | if ($method === 'GET') {
70 | foreach ($data as $key => $val) {
71 | $restRequest->set_param($key, $val);
72 | }
73 | } else {
74 | $restRequest->set_body_params($data);
75 | }
76 |
77 | /** @var \WP_REST_Response $restResponse */
78 | $restResponse = rest_do_request($restRequest);
79 | $responseData = $restResponse->get_data();
80 |
81 | // Handle pagination response wrapping
82 | if ($pagination && is_array($responseData)) {
83 | $headers = $restResponse->get_headers();
84 | $page = isset($data['page']) ? intval($data['page']) : 1;
85 | $perPage = isset($data['per_page']) ? intval($data['per_page']) : 10;
86 | $totalPages = isset($headers['X-WP-TotalPages']) ? intval($headers['X-WP-TotalPages']) : null;
87 |
88 | if ($totalPages && $page < $totalPages) {
89 | $nextCursor = base64_encode(json_encode([
90 | 'page' => $page + 1,
91 | 'per_page' => $perPage
92 | ]));
93 |
94 | $responseData = [
95 | 'results' => $responseData,
96 | 'nextCursor' => $nextCursor
97 | ];
98 | }
99 | }
100 |
101 | if (is_string($responseData)) {
102 | return $responseData;
103 | }
104 |
105 | return json_encode($responseData);
106 | }
107 |
108 |
109 | private function _getDefaults()
110 | {
111 | $defaults = $this->evaluateString($this->_defaults);
112 | if (\is_array($defaults)) {
113 | return $defaults;
114 | }
115 | return [];
116 | }
117 |
118 | public function accepts($functionName)
119 | {
120 | return $this->getName() === $functionName;
121 | }
122 |
123 | public function getName()
124 | {
125 | return $this->evaluateString($this->_name);
126 | }
127 |
128 | public function getDefinition()
129 | {
130 | $parameters = $this->getService()->evaluateArgs($this->_parameters, $this);
131 | if (empty($parameters)) {
132 | $parameters = new \stdClass();
133 | }
134 | return [
135 | 'name' => $this->getName(),
136 | 'description' => $this->evaluateString($this->_description),
137 | 'parameters' => [
138 | 'type' => 'object',
139 | 'properties' => $parameters,
140 | 'required' => $this->evaluateString($this->_required),
141 | ],
142 | ];
143 | }
144 |
145 |
146 | // TODO: refactor separate chat from scoped function
147 | public function initParams() {
148 | return '';
149 | }
150 |
151 | public function restoreParams($executionId) {}
152 |
153 | public function getFunctionParams()
154 | {
155 | return new SimpleParams();
156 | }
157 |
158 |
159 | // UTIL
160 | public function __toString()
161 | {
162 | return parent::__toString() . '[' . $this->_name . ']';
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/PluginContext.php:
--------------------------------------------------------------------------------
1 | _logger = $this->getContainer()->get('logger');
39 |
40 | // $this->_logger->debug('-------------------------------------------------------------');
41 | // $this->_logger->debug('Initializing Convoworks MCP Plugin Context');
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | private static function getConvoWPPluginClass()
48 | {
49 | if (!isset(self::$_convoWPPluginClass)) {
50 | if (class_exists('Convo\Wp\Providers\ConvoWPPlugin')) {
51 | self::$_convoWPPluginClass = 'Convo\Wp\Providers\ConvoWPPlugin';
52 | } elseif (class_exists('Convo\Providers\ConvoWPPlugin')) {
53 | self::$_convoWPPluginClass = 'Convo\Providers\ConvoWPPlugin';
54 | } else {
55 | throw new \Exception('ConvoWPPlugin class not found. Neither Convo\Wp\Providers\ConvoWPPlugin nor Convo\Providers\ConvoWPPlugin is available.');
56 | }
57 | }
58 | return self::$_convoWPPluginClass;
59 | }
60 |
61 | /**
62 | * @return \Psr\Container\ContainerInterface
63 | */
64 | public function getContainer()
65 | {
66 | $convoWPPluginClass = self::getConvoWPPluginClass();
67 |
68 | if (is_admin()) {
69 | return $convoWPPluginClass::getAdminDiContainer();
70 | }
71 |
72 | // if ( wp_doing_cron()) {
73 | // $container = $convoWPPluginClass::getPublicDiContainer();
74 | // } else {
75 | // $container = $convoWPPluginClass::getPublicDiContainer();
76 | // }
77 |
78 | return $convoWPPluginClass::getPublicDiContainer();
79 | }
80 |
81 | /**
82 | * @return \Psr\Log\LoggerInterface
83 | */
84 | public function getLogger()
85 | {
86 | return $this->_logger;
87 | }
88 |
89 | public function getSettingsViewModel()
90 | {
91 | if (!isset($this->_cache['settingsViewModel'])) {
92 | $this->_cache['settingsViewModel'] = new SettingsViewModel($this->getLogger(), $this->getContainer()->get('convoServiceDataProvider'));
93 | $this->_cache['settingsViewModel']->init();
94 | }
95 |
96 | return $this->_cache['settingsViewModel'];
97 | }
98 |
99 | /**
100 | * @return McpConvoworksManager
101 | */
102 | public function getMcpConvoworksManager()
103 | {
104 | if (!isset($this->_cache['mcpConvoworksManager'])) {
105 | $this->_cache['mcpConvoworksManager'] = new McpConvoworksManager(
106 | $this->getLogger(),
107 | $this->getMcpServerPackage(),
108 | $this->getContainer()->get('convoServiceDataProvider')
109 | );
110 | }
111 |
112 | return $this->_cache['mcpConvoworksManager'];
113 | }
114 |
115 | /**
116 | * @return IPackageDescriptor
117 | */
118 | public function getMcpServerPackage()
119 | {
120 | if (!isset($this->_cache['mcpPackage'])) {
121 | $container = $this->getContainer();
122 | $this->_cache['mcpPackage'] = new \Convo\Core\Factory\FunctionPackageDescriptor(
123 | '\Convo\Gpt\Pckg\GptPackageDefinition',
124 | function () use ($container) {
125 | $logger = $container->get('logger');
126 | $api_factory = new GptApiFactory($logger, $container->get('httpFactory'));
127 | $stream_writer = new StreamWriter($logger);
128 | $stream_handler = new StreamHandler($stream_writer, $logger);
129 |
130 | $mcp_manager_factory = new McpSessionManagerFactory(
131 | $logger,
132 | CONVO_GPT_MCP_SESSION_STORAGE_PATH
133 | );
134 |
135 | $command_dispatcher = new CommandDispatcher(
136 | $container->get('convoServiceFactory'),
137 | $container->get('convoServiceParamsFactory'),
138 | $logger,
139 | $mcp_manager_factory
140 | );
141 |
142 | $handler = new StreamableRestHandler(
143 | $logger,
144 | $container->get('httpFactory'),
145 | $container->get('convoServiceFactory'),
146 | $container->get('convoServiceDataProvider'),
147 | $mcp_manager_factory,
148 | $command_dispatcher,
149 | $stream_handler
150 | );
151 | $mcp_platform = new McpServerPlatform(
152 | $logger,
153 | $container->get('convoServiceDataProvider'),
154 | $container->get('serviceReleaseManager'),
155 | $handler
156 | );
157 | $logger->debug('Registering package [' . GptPackageDefinition::NAMESPACE . ']');
158 | return new GptPackageDefinition(
159 | $logger,
160 | $api_factory,
161 | $mcp_platform,
162 | $mcp_manager_factory
163 | );
164 | }
165 | );
166 | }
167 |
168 | return $this->_cache['mcpPackage'];
169 | }
170 |
171 | // UTIL
172 | public function __toString()
173 | {
174 | return get_class($this);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/system-message-element.md:
--------------------------------------------------------------------------------
1 | ### System Message
2 |
3 | Defines a system-generated message within the chat context for **GPT Chat Completion API v2**. System messages set the behavior, personality, and context for the AI assistant and are crucial for consistent AI responses.
4 |
5 | ### When to use
6 |
7 | Use **System Message** when you need to:
8 |
9 | - Provide instructions or context to the AI assistant
10 | - Set the AI's personality, tone, or role
11 | - Define the AI's capabilities or limitations
12 | - Include dynamic context (date, user info, site settings) in the prompt
13 | - Add transient instructions that shouldn't be stored in conversation history
14 |
15 | This element is typically used in the **Messages** (message_provider) flow of **GPT Chat Completion API v2**.
16 |
17 | ### Properties
18 |
19 | #### Message content (content)
20 |
21 | The text content of the system message. This can be static text or dynamic content using expression language.
22 |
23 | Example values:
24 |
25 | - `You are a helpful customer support assistant for ${site_name}.`
26 | - `Today is ${date("l, F j, Y")}. Answer questions about our products and services.`
27 | - `You are an expert in ${topic}. Provide detailed, accurate information.`
28 |
29 | The content is evaluated as an expression by default, allowing you to inject dynamic values.
30 |
31 | #### Disable evaluation (disable_eval)
32 |
33 | When enabled, prevents expression language evaluation in the content field. The content is used as literal text.
34 |
35 | Default: `false`
36 |
37 | Use this when:
38 |
39 | - You want to include `${...}` syntax as literal text (for demonstrating expression language)
40 | - You have pre-formatted content that shouldn't be evaluated
41 | - You want to avoid accidental expression evaluation in user-provided content
42 |
43 | ### Runtime behavior
44 |
45 | When the element executes:
46 |
47 | 1. The content is evaluated (unless `disable_eval` is true)
48 | 2. A system message object is registered with the parent `IMessages` container
49 | 3. The message is marked as `transient: true` by default
50 |
51 | **Transient flag**: System messages created by this element are marked as transient, meaning they are excluded from the conversation history returned by `getConversation()`. They are still sent to the API but don't appear in the persistent conversation array.
52 |
53 | ### Message structure
54 |
55 | The registered message has this structure:
56 |
57 | ```text
58 | {
59 | "role": "system",
60 | "transient": true,
61 | "content": ""
62 | }
63 | ```
64 |
65 | ### Integration with Chat Completion API v2
66 |
67 | **System Message** is typically used at the beginning of the **Messages** (message_provider) flow in **GPT Chat Completion API v2**:
68 |
69 | **Messages flow**:
70 |
71 | 1. **System Message**: Main instructions for the AI
72 | 2. **Conversation Messages**: Load conversation history
73 | 3. *[Optional]* Additional **System Message** elements for context-specific instructions
74 |
75 | **Why multiple system messages?**
76 |
77 | You can add multiple **System Message** elements to organize your prompts:
78 |
79 | - First system message: Core AI personality and role
80 | - Second system message: Dynamic context (date, user profile, current page)
81 | - Third system message: Task-specific instructions
82 |
83 | GPT will consider all system messages when generating responses.
84 |
85 | ### Example
86 |
87 | **Basic system message**:
88 |
89 | - **Message content**: `You are a friendly travel assistant. Provide concise information about destinations, hotels, and activities.`
90 | - **Disable evaluation**: `false`
91 |
92 | **Dynamic system message with date**:
93 |
94 | - **Message content**: `You are a helpful assistant. Today is ${date("l, F j, Y")}. The current time is ${date("H:i")}. Use this information when relevant to the user's query.`
95 | - **Disable evaluation**: `false`
96 |
97 | **User-personalized system message**:
98 |
99 | - **Message content**: `You are assisting ${user_name}, a ${user_role} at ${company_name}. Tailor your responses to their professional context.`
100 | - **Disable evaluation**: `false`
101 |
102 | ### Example: Multi-part system prompts
103 |
104 | **Messages flow in Chat Completion v2**:
105 |
106 | 1. **System Message**:
107 | - Content: `You are a WordPress site administrator assistant with access to the WordPress REST API.`
108 |
109 | 2. **System Message**:
110 | - Content: `Current site: ${site_url}. Site name: ${site_name}. Admin user: ${current_user.display_name}.`
111 |
112 | 3. **System Message**:
113 | - Content: `You can create, read, update, and delete posts, pages, and users. Always confirm destructive actions before executing them.`
114 |
115 | 4. **Conversation Messages**:
116 | - Messages: `${conversation}`
117 |
118 | ### Example: Literal expression syntax
119 |
120 | **When you need to show `${...}` as literal text**:
121 |
122 | - **Message content**: `To access variables in Convoworks, use this syntax: \${variable_name}`
123 | - **Disable evaluation**: `true`
124 |
125 | This will send the literal text with `${variable_name}` to GPT without evaluation.
126 |
127 | ### Example: Conditional system message
128 |
129 | **Add context only when needed**:
130 |
131 | **Messages flow**:
132 |
133 | 1. **System Message**:
134 | - Content: `You are a helpful assistant.`
135 |
136 | 2. **If** element: `${request.platform == "amazon"}`
137 | - **True flow**:
138 | - **System Message**:
139 | - Content: `You are running on Amazon Alexa. Keep responses concise and voice-friendly.`
140 |
141 | 3. **Conversation Messages**: `${conversation}`
142 |
143 | ### Tips
144 |
145 | - Keep system messages clear, specific, and concise – avoid overly long instructions
146 | - Place the most important instructions in the first system message
147 | - Use multiple system messages to separate concerns (role, context, rules)
148 | - Include the current date/time when temporal context matters: `${date("Y-m-d H:i:s")}`
149 | - System messages with `transient: true` won't be stored in the conversation array returned by Chat Completion v2
150 | - System messages are processed by GPT on every API call, so dynamic values (like date) are always fresh
151 | - For production chatbots, include usage guidelines (e.g., "Don't provide medical advice", "Don't share personal data")
152 | - Test different system prompts – small wording changes can significantly impact AI behavior
153 | - Avoid contradictory instructions across multiple system messages
154 | - For showing expression syntax as examples, use `disable_eval: true`
155 | - System messages appear as separate messages in the API request, so they count toward context length
156 |
157 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/McpFilesystemSessionStore.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
25 | $this->_basePath = $basePath;
26 | $this->_serviceId = $serviceId;
27 | }
28 |
29 | // create new session
30 | public function createSession($clientName): string
31 | {
32 | $session_id = StrUtil::uuidV4();
33 | $path = $this->_getServicePath() . $session_id;
34 | if (false === wp_mkdir_p($path)) {
35 | throw new \RuntimeException('Failed to create session directory: ' . $path);
36 | }
37 | $this->_logger->debug('Created new session directory: ' . $path);
38 |
39 | $session = [
40 | 'session_id' => $session_id,
41 | 'status' => IMcpSessionStoreInterface::SESSION_STATUS_NEW,
42 | 'clientName' => $clientName,
43 | 'created_at' => time(),
44 | 'last_active' => time(),
45 | ];
46 | $this->_saveSession($session);
47 |
48 | return $session_id;
49 | }
50 |
51 | public function saveSession($session): void
52 | {
53 | $this->_saveSession($session);
54 | }
55 |
56 |
57 |
58 | // COMMANDS
59 | public function initialiseSession($sessionId): void
60 | {
61 | $session = $this->getSession($sessionId);
62 |
63 | if ($session['status'] !== IMcpSessionStoreInterface::SESSION_STATUS_NEW) {
64 | throw new DataItemNotFoundException('No NEW session found: ' . $sessionId);
65 | }
66 |
67 | $session['status'] = IMcpSessionStoreInterface::SESSION_STATUS_INITIALISED;
68 | $session['last_active'] = time();
69 |
70 | $this->_saveSession($session);
71 | }
72 |
73 | // queues the notification (now queues full JSON-RPC message)
74 | public function queueEvent(string $sessionId, array $data): void
75 | {
76 | $queueFile = $this->_getQueueFile($sessionId);
77 | file_put_contents($queueFile, json_encode($data) . PHP_EOL, FILE_APPEND | LOCK_EX);
78 | $this->pingSession($sessionId);
79 | }
80 |
81 | public function queueEvents(string $sessionId, array $events): void
82 | {
83 | $queueFile = $this->_getQueueFile($sessionId);
84 | $lines = array_map(function ($e) {
85 | return json_encode($e);
86 | }, $events);
87 | file_put_contents($queueFile, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND | LOCK_EX);
88 | $this->pingSession($sessionId);
89 | }
90 |
91 | // read next message (deletes it)
92 | public function nextEvent($sessionId): ?array
93 | {
94 | $queueFile = $this->_getQueueFile($sessionId);
95 | if (!file_exists($queueFile) || filesize($queueFile) === 0) return null;
96 | $handle = fopen($queueFile, 'r+');
97 | flock($handle, LOCK_EX);
98 | $event = json_decode(fgets($handle), true);
99 | $remaining = fread($handle, filesize($queueFile));
100 | rewind($handle);
101 | ftruncate($handle, 0);
102 | fwrite($handle, $remaining);
103 | flock($handle, LOCK_UN);
104 | fclose($handle);
105 | return $event;
106 | }
107 |
108 | private function _getQueueFile(string $sessionId): string
109 | {
110 | return $this->_getServicePath() . $sessionId . '/queue.jsonl';
111 | }
112 |
113 | public function pingSession($sessionId): void
114 | {
115 | $session = $this->getSession($sessionId);
116 | $session['last_active'] = time();
117 | $this->_saveSession($session);
118 | }
119 |
120 | private function _getServicePath(): string
121 | {
122 | return $this->_basePath . $this->_serviceId . '/';
123 | }
124 |
125 | // PERSISTENCE
126 | public function getSession($sessionId): array
127 | {
128 | $path = $this->_getServicePath() . $sessionId . '.json';
129 | if (!is_file($path)) {
130 | throw new DataItemNotFoundException('Session file [' . $path . '] not found');
131 | }
132 |
133 | $session = json_decode(file_get_contents($path), true);
134 | if (empty($session)) {
135 | throw new \RuntimeException('Failed to decode session file: ' . $path);
136 | }
137 |
138 | return $session;
139 | }
140 |
141 | private function _saveSession($session)
142 | {
143 | $path = $this->_getServicePath() . $session['session_id'] . '.json';
144 | $jsonData = json_encode($session, JSON_PRETTY_PRINT);
145 | file_put_contents($path, $jsonData);
146 | }
147 |
148 |
149 | // UTIL
150 |
151 |
152 | /**
153 | * Deletes session files and folders that have been inactive for the given time (in seconds).
154 | *
155 | * @param int $inactiveTime Seconds of inactivity before deletion.
156 | * @return int Number of deleted sessions.
157 | */
158 | public function cleanupInactiveSessions(int $inactiveTime): int
159 | {
160 | $deleted = 0;
161 | $files = glob($this->_getServicePath() . '*.json');
162 | foreach ($files as $file) {
163 | $session = json_decode(file_get_contents($file), true);
164 | if (!$session || !isset($session['last_active'])) {
165 | continue;
166 | }
167 | if ($session['last_active'] < time() - $inactiveTime) {
168 | $sessionId = $session['session_id'];
169 | // Delete session file
170 | @unlink($file);
171 | // Delete queue file
172 | $queueFile = $this->_getQueueFile($sessionId);
173 | if (is_file($queueFile)) {
174 | @unlink($queueFile);
175 | }
176 | // Delete session folder
177 | $sessionFolder = $this->_getServicePath() . $sessionId;
178 | if (is_dir($sessionFolder)) {
179 | // Remove all files in folder
180 | $folderFiles = glob($sessionFolder . '/*');
181 | foreach ($folderFiles as $f) {
182 | @unlink($f);
183 | }
184 | @rmdir($sessionFolder);
185 | }
186 | $deleted++;
187 | $this->_logger->info("Deleted inactive session: $sessionId");
188 | }
189 | }
190 | return $deleted;
191 | }
192 |
193 | public function __toString()
194 | {
195 | return get_class($this) . '[]';
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/moderation-api-element.md:
--------------------------------------------------------------------------------
1 | ### GPT Moderation API
2 |
3 | Validates input content using the OpenAI Moderation API. This element helps ensure that user-generated content or AI responses comply with content policies by detecting potentially harmful, offensive, or policy-violating content.
4 |
5 | ### When to use
6 |
7 | Use **GPT Moderation API** when you need to:
8 |
9 | - Screen user input before processing it with GPT
10 | - Validate AI-generated content before displaying it to users
11 | - Implement content filtering and safety controls
12 | - Detect harmful, offensive, or inappropriate content
13 | - Comply with content moderation policies and regulations
14 | - Flag content for human review based on moderation scores
15 |
16 | ### Properties
17 |
18 | #### Input
19 |
20 | The text content to be moderated. This can be user input, AI-generated content, or any text you want to validate.
21 |
22 | Example values:
23 |
24 | - `${request.text}` – User's message
25 | - `${gpt_response.choices[0].message.content}` – AI response
26 | - `Any text string you want to check`
27 |
28 | #### API key
29 |
30 | Your OpenAI API key. Store this in a service variable for security.
31 |
32 | Example: `${GPT_API_KEY}`
33 |
34 | #### Base URL
35 |
36 | Optional. Base URL for the API endpoint. If left blank, defaults to `https://api.openai.com/v1`.
37 |
38 | Use this to point to custom endpoints if needed.
39 |
40 | #### API options
41 |
42 | Configuration options for the Moderation API:
43 |
44 | - **model** – The moderation model to use (e.g., `text-moderation-latest`, `text-moderation-stable`)
45 |
46 | Default value:
47 |
48 | ```text
49 | model: text-moderation-latest
50 | ```
51 |
52 | The `text-moderation-latest` model is automatically updated to use the most recent moderation model, while `text-moderation-stable` provides a stable version that only updates with advance notice.
53 |
54 | For details, see the [OpenAI Moderation API Reference](https://platform.openai.com/docs/api-reference/moderations).
55 |
56 | #### Result Variable Name
57 |
58 | The name of the variable that will store the moderation API response. Default is `status`.
59 |
60 | #### OK flow
61 |
62 | Sub-flow executed after the moderation check completes. The result variable will be available for use in this flow.
63 |
64 | Use this flow to:
65 |
66 | - Check moderation flags and take appropriate action
67 | - Block content that violates policies
68 | - Log flagged content for review
69 | - Provide user feedback about content violations
70 |
71 | ### Runtime behavior
72 |
73 | When the element executes:
74 |
75 | 1. The input text is evaluated
76 | 2. An API call is made to OpenAI Moderation endpoint
77 | 3. The moderation response is stored in the result variable (request scope)
78 | 4. The OK flow is executed with the result variable available
79 |
80 | ### Response structure
81 |
82 | The result variable contains moderation analysis with:
83 |
84 | - `${status.results[0].flagged}` – Boolean indicating if content was flagged
85 | - `${status.results[0].categories}` – Object with category flags (true/false for each category)
86 | - `${status.results[0].category_scores}` – Object with confidence scores (0.0-1.0) for each category
87 |
88 | **Moderation categories**:
89 |
90 | - `hate` – Content promoting hate based on identity
91 | - `hate/threatening` – Hateful content with violence or harm
92 | - `harassment` – Harassing, bullying, or abusive content
93 | - `harassment/threatening` – Harassment with threats of harm
94 | - `self-harm` – Content promoting self-harm
95 | - `self-harm/intent` – Intent to engage in self-harm
96 | - `self-harm/instructions` – Instructions for self-harm
97 | - `sexual` – Sexual content
98 | - `sexual/minors` – Sexual content involving minors
99 | - `violence` – Content promoting violence
100 | - `violence/graphic` – Graphic violent content
101 |
102 | ### Example
103 |
104 | **Moderate user input before sending to GPT**:
105 |
106 | **Configuration**:
107 |
108 | - **Input**: `${request.text}`
109 | - **API key**: `${GPT_API_KEY}`
110 | - **Result Variable Name**: `moderation`
111 |
112 | **In the OK flow**, add conditional logic:
113 |
114 | 1. Add **If** element with condition: `${moderation.results[0].flagged}`
115 | - **True flow**: Add **Text Response**: `I'm sorry, I can't process that request. Please rephrase your message to comply with our content policy.`
116 | - Add **Stop** element to prevent further processing
117 | - **False flow**: Continue with normal GPT processing
118 |
119 | ### Example: Log flagged content
120 |
121 | **In the OK flow**, add logging for flagged content:
122 |
123 | 1. Add **If** element: `${moderation.results[0].flagged}`
124 | - **True flow**:
125 | - Add **Log Message**: `Flagged content detected: ${request.text}`
126 | - Add **Set Param** (session scope):
127 | - Name: `flagged_messages`
128 | - Value: `${array_merge(flagged_messages, [{"text": request.text, "categories": moderation.results[0].categories, "timestamp": date("Y-m-d H:i:s")}])}`
129 |
130 | ### Example: Category-specific handling
131 |
132 | **Handle different categories differently**:
133 |
134 | 1. Add **If** element: `${moderation.results[0].categories.harassment}`
135 | - **True flow**: Block and log as harassment
136 | 2. Add **Else If** element: `${moderation.results[0].category_scores.violence > 0.8}`
137 | - **True flow**: Flag for manual review (high violence score)
138 | 3. Add **Else** flow: Continue normally
139 |
140 | ### Example: Two-way moderation
141 |
142 | **Moderate both user input and AI responses**:
143 |
144 | **Step 1 – Moderate user input**:
145 |
146 | - Use **GPT Moderation API** on `${request.text}`
147 | - Block flagged input
148 |
149 | **Step 2 – Process with GPT** (if input is clean)
150 |
151 | **Step 3 – Moderate GPT response**:
152 |
153 | - Use another **GPT Moderation API** on `${gpt_response.choices[0].message.content}`
154 | - If flagged, regenerate with adjusted parameters or use a fallback response
155 |
156 | ### Tips
157 |
158 | - Always moderate user input before processing expensive GPT API calls – prevents wasting tokens on policy-violating content
159 | - Consider moderating AI-generated content as well – even with safety settings, GPT can occasionally produce borderline content
160 | - Use `category_scores` for fine-grained control – you can set your own thresholds stricter than the default `flagged` boolean
161 | - Log all flagged content with timestamps and user IDs for compliance and improvement
162 | - Provide clear, helpful error messages to users when content is blocked
163 | - For chatbots, implement progressive warnings – first warning, then temporary restrictions, then account restrictions for repeated violations
164 | - Use `text-moderation-latest` in development, `text-moderation-stable` in production if you need predictable behavior
165 | - The Moderation API is free to use (as of current OpenAI pricing) – don't hesitate to use it liberally
166 | - Combine with application-level filtering for comprehensive content safety
167 | - Test edge cases – the API may not catch all policy violations, and it may occasionally flag acceptable content (false positives)
168 |
169 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Pckg/Help/simple-message-limiter-element.md:
--------------------------------------------------------------------------------
1 | ### Simple Messages Limiter
2 |
3 | Limits the size of the conversation by trimming old messages based on token count. Unlike **GPT Messages Limiter**, this element does not summarize – it simply removes the oldest messages when the conversation exceeds the specified limit.
4 |
5 | ### When to use
6 |
7 | Use **Simple Messages Limiter** when you need to:
8 |
9 | - Keep conversation context within model token limits
10 | - Remove old messages without summarization (faster, no API cost)
11 | - Implement a sliding window conversation approach
12 | - Prevent context length overflow errors
13 | - Manage long-running conversations efficiently
14 |
15 | For conversations where context from old messages is important, consider using **GPT Messages Limiter** instead, which summarizes removed messages.
16 |
17 | ### Properties
18 |
19 | #### Max Tokens to Keep
20 |
21 | The maximum estimated token count allowed in the conversation before trimming. When the total token count of messages exceeds this limit, old messages are removed.
22 |
23 | Example: `${8192}`
24 |
25 | Tokens are estimated using a simple calculation (not exact tokenization).
26 |
27 | #### Truncate to Tokens
28 |
29 | The estimated token count to retain after trimming. This should be lower than **Max Tokens to Keep** to provide a buffer before the next truncation.
30 |
31 | Example: `${4096}`
32 |
33 | When truncation is triggered, messages are removed from the beginning until the total is below this value.
34 |
35 | #### Max messages to keep (Deprecated)
36 |
37 | Legacy parameter for message count-based truncation. Use **Max Tokens to Keep** instead for better control.
38 |
39 | #### Truncate message count (Deprecated)
40 |
41 | Legacy parameter for count-based truncation. Use **Truncate to Tokens** instead.
42 |
43 | #### Result Variable Name
44 |
45 | The name of the variable available in the **Truncated Flow**. Default is `status`.
46 |
47 | The variable contains:
48 |
49 | - `${status.messages}` – Array of remaining messages after truncation
50 | - `${status.truncated}` – Array of messages that were removed
51 |
52 | #### Messages (message_provider)
53 |
54 | A sub-flow that provides the conversation messages to be checked and potentially truncated. Typically contains **Conversation Messages** or **System Message** elements.
55 |
56 | #### Truncated Flow
57 |
58 | Flow executed when messages are truncated. This flow runs **after** truncation but **before** messages are passed to the parent container.
59 |
60 | Use this flow to:
61 |
62 | - Log truncation events
63 | - Update conversation metadata
64 | - Notify the user about removed messages
65 | - Store truncated messages for later reference
66 |
67 | ### Runtime behavior
68 |
69 | When the element executes:
70 |
71 | 1. The message_provider sub-flow runs, collecting messages
72 | 2. Total token count is estimated for all messages
73 | 3. If total exceeds **Max Tokens to Keep**:
74 | - Messages are removed from the beginning (oldest first)
75 | - Removal continues until total is below **Truncate to Tokens**
76 | - Truncated Flow executes with result variable containing remaining and removed messages
77 | 4. Remaining messages are registered with the parent `IMessages` container
78 |
79 | **Truncation strategy**: Messages are removed from the start of the array (oldest messages first). System messages and recent messages are preserved.
80 |
81 | ### Token estimation
82 |
83 | Token count is estimated using a simplified formula:
84 |
85 | - Text is split by whitespace and punctuation
86 | - Each word/token is counted
87 | - Overhead for message structure is added
88 |
89 | This is not exact GPT tokenization but provides a reasonable approximation for context management.
90 |
91 | ### Example
92 |
93 | **Basic conversation limiting**:
94 |
95 | **Simple Messages Limiter**:
96 |
97 | - **Max Tokens to Keep**: `${8000}`
98 | - **Truncate to Tokens**: `${4000}`
99 | - **Result Variable Name**: `status`
100 | - **Messages** sub-flow:
101 | - **Conversation Messages**: `${conversation}`
102 | - **Truncated Flow**:
103 | - **Log Message**: `Truncated ${count(status.truncated)} messages from conversation`
104 |
105 | **How it works**:
106 |
107 | 1. Conversation starts with 0 messages
108 | 2. Messages accumulate in `${conversation}` session variable
109 | 3. When total exceeds 8000 estimated tokens, oldest messages are removed
110 | 4. Truncation continues until total is below 4000 tokens
111 | 5. Log message records truncation event
112 | 6. Remaining messages are sent to GPT
113 |
114 | ### Example: User notification
115 |
116 | **Notify user when truncation occurs**:
117 |
118 | **Truncated Flow**:
119 |
120 | 1. **Set Param** (request scope):
121 | - Name: `show_truncation_notice`
122 | - Value: `${true}`
123 |
124 | 2. **Log Message**: `Removed ${count(status.truncated)} old messages (estimated ${estimate_tokens(serialize_gpt_messages(status.truncated))} tokens)`
125 |
126 | **OK Flow** (in Chat Completion v2):
127 |
128 | 1. **If** element: `${show_truncation_notice}`
129 | - **True flow**: **Text Response**: `Note: I've cleared some older messages from our conversation to free up space for new information.`
130 |
131 | ### Example: Nested with system messages
132 |
133 | **Include system messages in truncation check**:
134 |
135 | **Messages flow in Chat Completion v2**:
136 |
137 | 1. **Simple Messages Limiter**:
138 | - **Max Tokens to Keep**: `${10000}`
139 | - **Truncate to Tokens**: `${5000}`
140 | - **Messages** sub-flow:
141 | 1. **System Message**: `You are a helpful assistant. Today is ${date("Y-m-d")}.`
142 | 2. **Conversation Messages**: `${conversation}`
143 |
144 | **Result**: System messages are included in token count but typically preserved since they're at the end of the array.
145 |
146 | ### Comparison: Simple vs. GPT Messages Limiter
147 |
148 | | Feature | Simple Messages Limiter | GPT Messages Limiter |
149 | |---------|------------------------|---------------------|
150 | | **Method** | Removes old messages | Summarizes old messages |
151 | | **API cost** | Free (no API calls) | Costs tokens (uses Chat Completion API) |
152 | | **Speed** | Fast | Slower (waits for API) |
153 | | **Context preservation** | None (messages lost) | Good (summary preserved) |
154 | | **Best for** | Less important conversations, cost optimization | Important conversations requiring context |
155 |
156 | ### Tips
157 |
158 | - Set **Max Tokens to Keep** to about 75% of your model's context window
159 | - Set **Truncate to Tokens** to about 50% to provide headroom before next truncation
160 | - For GPT-4o (128k context), you might use max: 90000, truncate: 60000
161 | - For GPT-4 (8k context), use max: 6000, truncate: 3000
162 | - Always keep at least a few recent messages – don't truncate too aggressively
163 | - Use **Simple Messages Limiter** for cost-sensitive applications
164 | - Use **GPT Messages Limiter** when conversation context is critical
165 | - Monitor `${count(status.truncated)}` in the truncated flow to track truncation frequency
166 | - Consider storing `${status.truncated}` in a log or database for conversation history analysis
167 | - System messages added after this element are not included in truncation (they're added later)
168 | - Nested multiple limiters to implement tiered truncation strategies (e.g., remove then summarize)
169 |
170 |
--------------------------------------------------------------------------------
/src/Convo/Gpt/Mcp/CommandDispatcher.php:
--------------------------------------------------------------------------------
1 | _convoServiceFactory = $convoServiceFactory;
47 | $this->_convoServiceParamsFactory = $convoServiceParamsFactory;
48 | $this->_logger = $logger;
49 | $this->_mcpSessionManagerFactory = $mcpSessionManagerFactory;
50 | }
51 |
52 | /**
53 | * Processes incoming data (single or batch) and returns responses.
54 | *
55 | * @param array $data
56 | * @param string $sessionId
57 | * @param string $variant
58 | * @param string $serviceId
59 | * @return array
60 | */
61 | public function processIncoming(array $data, string $sessionId, string $variant, string $serviceId): array
62 | {
63 | $this->_mcpSessionManagerFactory->getSessionManager($serviceId)->getActiveSession($sessionId, true);
64 |
65 | if (array_keys($data) === range(0, \count($data) - 1)) {
66 | $this->_logger->warning('Batching not supported in 2025-06-18; processing first message only');
67 | $data = $data[0];
68 | }
69 |
70 | // Handle notifications (no 'id')
71 | if (!isset($data['id'])) {
72 | $this->_logger->debug('Processing notification [' . $data['method'] . ']; no response needed');
73 | $owner = new RestSystemUser();
74 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP;
75 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant);
76 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory);
77 |
78 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $data, $role);
79 | $text_response = new SseResponse($sessionId);
80 | $text_response->setLogger($this->_logger);
81 |
82 | try {
83 | $this->_logger->info('Running service instance [' . $service->getId() . '] for notification [' . $data['method'] . ']');
84 | $service->run($text_request, $text_response);
85 | } catch (Throwable $e) {
86 | $this->_logger->error('Error processing notification [' . $data['method'] . ']: ' . $e->getMessage());
87 | }
88 |
89 | return []; // No response body for notifications
90 | }
91 |
92 | $owner = new RestSystemUser();
93 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP;
94 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant);
95 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory);
96 |
97 | $req_id = $data['id'];
98 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $data, $role);
99 | $text_response = new SseResponse($sessionId);
100 | $text_response->setLogger($this->_logger);
101 |
102 | try {
103 | $this->_logger->info('Running service instance [' . $service->getId() . '] in MCP POST Handler.');
104 | $service->run($text_request, $text_response);
105 | } catch (Throwable $e) {
106 | /** @phpstan-ignore-next-line */
107 | $this->_logger->error($e);
108 | return ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]];
109 | }
110 |
111 | $result = $text_response->getPlatformResponse();
112 | if (\is_array($result) && empty($result)) {
113 | $this->_logger->warning('Empty result array detected; converting to empty object');
114 | $result = new \stdClass();
115 | }
116 |
117 | return ['jsonrpc' => '2.0', 'id' => $req_id, 'result' => $result];
118 | }
119 |
120 | /**
121 | * Processes a single incoming message (for bidirectional streaming).
122 | *
123 | * @param array $message
124 | * @param string $sessionId
125 | * @param string $variant
126 | * @param string $serviceId
127 | */
128 | public function processMessage(
129 | array $message,
130 | string $sessionId,
131 | string $variant,
132 | string $serviceId
133 | ): void {
134 | $owner = new RestSystemUser();
135 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP;
136 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant);
137 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory);
138 |
139 | $manager = $this->_mcpSessionManagerFactory->getSessionManager($serviceId);
140 |
141 | if (array_keys($message) === range(0, \count($message) - 1)) {
142 | $responses = [];
143 | foreach ($message as $single_msg) {
144 | $req_id = $single_msg['id'];
145 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $single_msg, $role);
146 | $text_response = new SseResponse($sessionId);
147 | $text_response->setLogger($this->_logger);
148 | try {
149 | $service->run($text_request, $text_response);
150 | } catch (Throwable $e) {
151 | $responses[] = ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]];
152 | }
153 | }
154 | if (!empty($responses)) {
155 | $manager->enqueueMessage($sessionId, $responses);
156 | }
157 | } else {
158 | $req_id = $message['id'];
159 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $message, $role);
160 | $text_response = new SseResponse($sessionId);
161 | $text_response->setLogger($this->_logger);
162 | try {
163 | $service->run($text_request, $text_response);
164 | } catch (Throwable $e) {
165 | $error = ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]];
166 | $manager->enqueueMessage($sessionId, $error);
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------