feat(astrolabe): add Nextcloud PHP app for MCP server management
Adds a native Nextcloud app "Astroglobe" that provides: - Personal settings: OAuth authorization for background MCP access - Admin settings: Server status and vector sync monitoring - API endpoints for MCP server communication The app uses PKCE OAuth flow to obtain tokens for the MCP server, enabling features like background vector sync per ADR-018. Includes: - PHP app structure (controllers, services, settings) - Vue.js frontend components - Docker compose mount configuration - Installation hook for development testing - ADR-018 documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Astroglobe\Settings;
|
||||
|
||||
use OCA\Astroglobe\AppInfo\Application;
|
||||
use OCA\Astroglobe\Service\McpServerClient;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
/**
|
||||
* Admin settings panel for MCP Server.
|
||||
*
|
||||
* Displays server status, vector sync metrics, configuration,
|
||||
* and provides administrative controls.
|
||||
*/
|
||||
class Admin implements ISettings {
|
||||
private $client;
|
||||
private $config;
|
||||
private $initialState;
|
||||
|
||||
public function __construct(
|
||||
McpServerClient $client,
|
||||
IConfig $config,
|
||||
IInitialState $initialState
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->config = $config;
|
||||
$this->initialState = $initialState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm(): TemplateResponse {
|
||||
// Fetch data from MCP server
|
||||
$serverStatus = $this->client->getStatus();
|
||||
$vectorSyncStatus = $this->client->getVectorSyncStatus();
|
||||
|
||||
// Get configuration from config.php
|
||||
$serverUrl = $this->config->getSystemValue('mcp_server_url', '');
|
||||
$apiKeyConfigured = !empty($this->config->getSystemValue('mcp_server_api_key', ''));
|
||||
|
||||
// Check for server connection error
|
||||
if (isset($serverStatus['error'])) {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/error',
|
||||
[
|
||||
'error' => 'Cannot connect to MCP server',
|
||||
'details' => $serverStatus['error'],
|
||||
'server_url' => $serverUrl,
|
||||
'help_text' => 'Ensure MCP server is running and accessible. Check config.php for correct mcp_server_url.',
|
||||
],
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
// Provide initial state for Vue.js frontend (if needed)
|
||||
$this->initialState->provideInitialState('server-data', [
|
||||
'serverStatus' => $serverStatus,
|
||||
'vectorSyncStatus' => $vectorSyncStatus,
|
||||
'config' => [
|
||||
'serverUrl' => $serverUrl,
|
||||
'apiKeyConfigured' => $apiKeyConfigured,
|
||||
],
|
||||
]);
|
||||
|
||||
$parameters = [
|
||||
'serverStatus' => $serverStatus,
|
||||
'vectorSyncStatus' => $vectorSyncStatus,
|
||||
'serverUrl' => $serverUrl,
|
||||
'apiKeyConfigured' => $apiKeyConfigured,
|
||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||
];
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/admin',
|
||||
$parameters,
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The section ID
|
||||
*/
|
||||
public function getSection(): string {
|
||||
return 'mcp';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Priority (lower = higher up)
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Astroglobe\Settings;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
/**
|
||||
* Admin settings section for MCP Server.
|
||||
*
|
||||
* Creates a dedicated section in admin settings for MCP-related configuration.
|
||||
*/
|
||||
class AdminSection implements IIconSection {
|
||||
private $l;
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
|
||||
$this->l = $l;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The section ID
|
||||
*/
|
||||
public function getID(): string {
|
||||
return 'mcp';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The translated section name
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->l->t('MCP Server');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Priority (lower = higher up in list)
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Section icon (SVG or image URL)
|
||||
*/
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app.svg');
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Astroglobe\Settings;
|
||||
|
||||
use OCA\Astroglobe\AppInfo\Application;
|
||||
use OCA\Astroglobe\Service\McpServerClient;
|
||||
use OCA\Astroglobe\Service\McpTokenStorage;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
/**
|
||||
* Personal settings panel for MCP Server.
|
||||
*
|
||||
* Displays user session information, background access status,
|
||||
* and provides controls for managing MCP server integration.
|
||||
*
|
||||
* Uses OAuth PKCE flow - each user must authorize access to MCP server.
|
||||
*/
|
||||
class Personal implements ISettings {
|
||||
private $client;
|
||||
private $userSession;
|
||||
private $initialState;
|
||||
private $tokenStorage;
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
McpServerClient $client,
|
||||
IUserSession $userSession,
|
||||
IInitialState $initialState,
|
||||
McpTokenStorage $tokenStorage,
|
||||
IURLGenerator $urlGenerator
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->userSession = $userSession;
|
||||
$this->initialState = $initialState;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm(): TemplateResponse {
|
||||
$user = $this->userSession->getUser();
|
||||
if (!$user) {
|
||||
return new TemplateResponse(Application::APP_ID, 'settings/error', [
|
||||
'error' => 'User not authenticated'
|
||||
], TemplateResponse::RENDER_AS_BLANK);
|
||||
}
|
||||
|
||||
$userId = $user->getUID();
|
||||
|
||||
// Check if user has MCP OAuth token
|
||||
$token = $this->tokenStorage->getUserToken($userId);
|
||||
|
||||
// If no token or token is expired, show OAuth authorization UI
|
||||
if (!$token || $this->tokenStorage->isExpired($token)) {
|
||||
$oauthUrl = $this->urlGenerator->linkToRoute('astroglobe.oauth.initiateOAuth');
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/oauth-required',
|
||||
[
|
||||
'oauth_url' => $oauthUrl,
|
||||
'server_url' => $this->client->getPublicServerUrl(),
|
||||
'has_expired' => ($token !== null), // true if token exists but expired
|
||||
],
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
// User has valid token - fetch data from MCP server
|
||||
$accessToken = $token['access_token'];
|
||||
|
||||
// Fetch server status (public endpoint, no token needed)
|
||||
$serverStatus = $this->client->getStatus();
|
||||
|
||||
// Fetch user session data (requires token)
|
||||
$userSession = $this->client->getUserSession($userId, $accessToken);
|
||||
|
||||
// Check for server connection error
|
||||
if (isset($serverStatus['error'])) {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/error',
|
||||
[
|
||||
'error' => 'Cannot connect to MCP server',
|
||||
'details' => $serverStatus['error'],
|
||||
'server_url' => $this->client->getPublicServerUrl(),
|
||||
],
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
// Check for authentication error (invalid/expired token)
|
||||
if (isset($userSession['error'])) {
|
||||
// Token might be invalid - delete it and show OAuth UI
|
||||
$this->tokenStorage->deleteUserToken($userId);
|
||||
|
||||
$oauthUrl = $this->urlGenerator->linkToRoute('astroglobe.oauth.initiateOAuth');
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/oauth-required',
|
||||
[
|
||||
'oauth_url' => $oauthUrl,
|
||||
'server_url' => $this->client->getPublicServerUrl(),
|
||||
'has_expired' => true,
|
||||
'error_message' => 'Your session has expired. Please sign in again.',
|
||||
],
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
// Provide initial state for Vue.js frontend (if needed)
|
||||
$this->initialState->provideInitialState('user-data', [
|
||||
'userId' => $userId,
|
||||
'serverStatus' => $serverStatus,
|
||||
'session' => $userSession,
|
||||
]);
|
||||
|
||||
$parameters = [
|
||||
'userId' => $userId,
|
||||
'serverStatus' => $serverStatus,
|
||||
'session' => $userSession,
|
||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||
'backgroundAccessGranted' => $userSession['background_access_granted'] ?? false,
|
||||
'serverUrl' => $this->client->getPublicServerUrl(),
|
||||
'hasToken' => true,
|
||||
];
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'settings/personal',
|
||||
$parameters,
|
||||
TemplateResponse::RENDER_AS_BLANK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The section ID
|
||||
*/
|
||||
public function getSection(): string {
|
||||
return 'mcp';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Priority (lower = higher up)
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Astroglobe\Settings;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
/**
|
||||
* Personal settings section for MCP Server.
|
||||
*
|
||||
* Creates a dedicated section in personal settings for MCP-related configuration.
|
||||
*/
|
||||
class PersonalSection implements IIconSection {
|
||||
private $l;
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
|
||||
$this->l = $l;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The section ID (e.g. 'mcp')
|
||||
*/
|
||||
public function getID(): string {
|
||||
return 'mcp';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The translated section name
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->l->t('MCP Server');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Priority (lower = higher up in list, 0-99)
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Section icon (SVG or image URL)
|
||||
*/
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app.svg');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user