Skip to content

Commit c0892eb

Browse files
authored
Merge pull request #34 from dreamfactorysoftware/feature/add-github-integration
add dreamfactory utility service integration for github to df-mcp cus…
2 parents f384b29 + 6a2c278 commit c0892eb

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

config/mcp.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?php
22

33
return [
4+
// Cache TTL (seconds) for function bodies fetched from SCM services (GitHub/GitLab/Bitbucket)
5+
'scm_cache_ttl' => env('MCP_SCM_CACHE_TTL', 300),
6+
47
// Daemon configuration
58
'daemon' => [
69
'enabled' => env('MCP_DAEMON_ENABLED', true),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class AddScmFieldsToMcpCustomTools extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::table('mcp_custom_tools', function (Blueprint $table) {
12+
$table->unsignedInteger('storage_service_id')->nullable()->after('function');
13+
$table->string('scm_repository')->nullable()->after('storage_service_id');
14+
$table->string('scm_reference')->nullable()->after('scm_repository');
15+
$table->string('storage_path')->nullable()->after('scm_reference');
16+
17+
$table->foreign('storage_service_id')
18+
->references('id')
19+
->on('service')
20+
->onDelete('set null');
21+
});
22+
}
23+
24+
public function down()
25+
{
26+
Schema::table('mcp_custom_tools', function (Blueprint $table) {
27+
$table->dropForeign(['storage_service_id']);
28+
$table->dropColumn(['storage_service_id', 'scm_repository', 'scm_reference', 'storage_path']);
29+
});
30+
}
31+
}

src/Http/Controllers/McpStreamController.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use DreamFactory\Core\Http\Controllers\Controller;
66
use DreamFactory\Core\McpServer\Client\McpDaemonClient;
7+
use DreamFactory\Core\McpServer\Models\McpCustomTool;
78
use DreamFactory\Core\McpServer\Models\McpOAuthAccessToken;
89
use DreamFactory\Core\Enums\ServiceTypeGroups;
910
use DreamFactory\Core\Utility\Session as SessionUtilities;
@@ -80,6 +81,11 @@ private function processMcpRequest(Request $request, string $mcpService)
8081
$config['custom_tools'][$i]['function'] = $body;
8182
}
8283

84+
// Resolve SCM-linked function bodies from GitHub/GitLab/Bitbucket.
85+
// Must run before extractFunctionSecrets so secrets.KEY references
86+
// in GitHub-hosted code are found and resolved.
87+
$this->resolveScmFunctionBodies($config['custom_tools']);
88+
8389
// Scan function bodies for secrets.KEY references, resolve the lookup
8490
// values, and attach a secrets map so the daemon can inject them at runtime
8591
// without the values ever appearing in the JS source string.
@@ -153,6 +159,36 @@ private function extractFunctionSecrets(array &$customTools): void
153159
}
154160
}
155161

162+
/**
163+
* Resolve function bodies from linked SCM services for function-type custom tools.
164+
*
165+
* For each tool with a storage_service_id, fetches the function body from the
166+
* configured GitHub/GitLab/Bitbucket service and overwrites the function field.
167+
* If the fetch fails, the existing inline function body (if any) is kept as fallback.
168+
*/
169+
private function resolveScmFunctionBodies(array &$customTools): void
170+
{
171+
foreach ($customTools as &$tool) {
172+
$toolType = $tool['tool_type'] ?? 'api';
173+
$storageServiceId = $tool['storage_service_id'] ?? null;
174+
175+
if ($toolType !== 'function' || empty($storageServiceId)) {
176+
continue;
177+
}
178+
179+
$content = McpCustomTool::resolveScmFunctionBody(
180+
$storageServiceId,
181+
$tool['scm_repository'] ?? null,
182+
$tool['scm_reference'] ?? null,
183+
$tool['storage_path'] ?? null
184+
);
185+
186+
if ($content !== null) {
187+
$tool['function'] = $content;
188+
}
189+
}
190+
}
191+
156192
/**
157193
* Get available database and file services, filtered by the user's role.
158194
*

src/Models/McpCustomTool.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace DreamFactory\Core\McpServer\Models;
44

5+
use DreamFactory\Core\Enums\ServiceTypeGroups;
6+
use DreamFactory\Core\Enums\Verbs;
57
use DreamFactory\Core\Models\BaseModel;
68
use Illuminate\Database\Eloquent\SoftDeletes;
9+
use Illuminate\Support\Facades\Cache;
10+
use Illuminate\Support\Facades\Log;
711

812
class McpCustomTool extends BaseModel
913
{
@@ -22,10 +26,15 @@ class McpCustomTool extends BaseModel
2226
'headers',
2327
'function',
2428
'enabled',
29+
'storage_service_id',
30+
'scm_repository',
31+
'scm_reference',
32+
'storage_path',
2533
];
2634

2735
protected $casts = [
2836
'service_id' => 'integer',
37+
'storage_service_id' => 'integer',
2938
'parameters' => 'array',
3039
'headers' => 'array',
3140
'enabled' => 'boolean',
@@ -98,6 +107,14 @@ public static function syncToolsForService(int $serviceId, array $tools): void
98107
if (!empty($toolData['id']) && $existing->has($toolData['id'])) {
99108
$tool = $existing->get($toolData['id']);
100109
$tool->update($toolData);
110+
if ($tool->wasChanged(['storage_service_id', 'storage_path', 'scm_repository', 'scm_reference'])) {
111+
Cache::forget(self::buildScmCacheKey(
112+
$tool->storage_service_id,
113+
$tool->scm_repository,
114+
$tool->storage_path,
115+
$tool->scm_reference
116+
));
117+
}
101118
$receivedIds[] = $tool->id;
102119
} else {
103120
$tool = static::create($toolData);
@@ -126,6 +143,84 @@ public function toToolDefinition(): array
126143
'parameters' => $this->parameters ?? [],
127144
'headers' => !empty($this->headers) ? $this->headers : (object) [],
128145
'function' => $this->getAttribute('function'),
146+
'storage_service_id' => $this->storage_service_id,
147+
'scm_repository' => $this->scm_repository,
148+
'scm_reference' => $this->scm_reference,
149+
'storage_path' => $this->storage_path,
129150
];
130151
}
152+
153+
/**
154+
* Resolve a function body from a linked SCM service (GitHub/GitLab/Bitbucket).
155+
*
156+
* Uses TTL-based caching to avoid hitting the SCM API on every request.
157+
* Returns null if no SCM link is configured or if the fetch fails.
158+
*/
159+
public static function resolveScmFunctionBody(
160+
?int $storageServiceId,
161+
?string $scmRepository,
162+
?string $scmReference,
163+
?string $storagePath
164+
): ?string {
165+
if (empty($storageServiceId) || empty($storagePath)) {
166+
return null;
167+
}
168+
169+
$cacheKey = self::buildScmCacheKey($storageServiceId, $scmRepository, $storagePath, $scmReference);
170+
$ttl = config('mcp.scm_cache_ttl', 300);
171+
172+
return Cache::remember($cacheKey, $ttl, function () use ($storageServiceId, $scmRepository, $scmReference, $storagePath) {
173+
try {
174+
$service = \ServiceManager::getServiceById($storageServiceId);
175+
$serviceName = $service->getName();
176+
$typeGroup = $service->getServiceTypeInfo()->getGroup();
177+
$storagePath = trim($storagePath, '/');
178+
$scmRef = !empty($scmReference) ? $scmReference : null;
179+
180+
if ($typeGroup === ServiceTypeGroups::SCM) {
181+
$result = \ServiceManager::handleRequest(
182+
$serviceName,
183+
Verbs::GET,
184+
'_repo/' . $scmRepository,
185+
['path' => $storagePath, 'branch' => $scmRef, 'content' => 1]
186+
);
187+
188+
return $result->getContent();
189+
}
190+
191+
Log::warning('MCP custom tool storage_service_id does not point to an SCM service.', [
192+
'storage_service_id' => $storageServiceId,
193+
'type_group' => $typeGroup,
194+
]);
195+
196+
return null;
197+
} catch (\Exception $e) {
198+
Log::warning('Failed to fetch MCP custom tool function from SCM.', [
199+
'storage_service_id' => $storageServiceId,
200+
'scm_repository' => $scmRepository,
201+
'storage_path' => $storagePath,
202+
'error' => $e->getMessage(),
203+
]);
204+
205+
return null;
206+
}
207+
});
208+
}
209+
210+
/**
211+
* Build a consistent cache key for SCM-sourced function bodies.
212+
*/
213+
private static function buildScmCacheKey(
214+
?int $storageServiceId,
215+
?string $scmRepository,
216+
?string $storagePath,
217+
?string $scmReference
218+
): string {
219+
return 'mcp_scm_function:' . implode(':', [
220+
$storageServiceId ?? '',
221+
$scmRepository ?? '',
222+
$storagePath ?? '',
223+
$scmReference ?? '',
224+
]);
225+
}
131226
}

0 commit comments

Comments
 (0)