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:
Chris Coutinho
2025-12-14 20:11:21 +01:00
parent a58a14111b
commit 21817543ad
72 changed files with 27253 additions and 0 deletions
+101
View File
@@ -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;
}
}
+52
View File
@@ -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
View File
@@ -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;
}
}
+52
View File
@@ -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');
}
}