MCP Feature added
This commit is contained in:
parent
e70f395772
commit
b78c91d6c1
30
api/data_snapshot.php
Normal file
30
api/data_snapshot.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
$allowed = [
|
||||
'people' => 'people.json',
|
||||
'chores' => 'chores.json',
|
||||
'stores' => 'stores.json',
|
||||
'grocery_lists' => 'grocery_lists.json',
|
||||
'grocery_catalog' => 'grocery_catalog.json',
|
||||
'meals' => 'meals.json',
|
||||
'meal_plans' => 'meal_plans.json',
|
||||
'family_settings' => 'family_settings.json',
|
||||
'expenses' => 'expenses.json',
|
||||
];
|
||||
|
||||
$type = isset($_GET['type']) ? trim((string) $_GET['type']) : '';
|
||||
if ($type === '' || !isset($allowed[$type])) {
|
||||
sendJson(['success' => false, 'error' => 'type must be one of: ' . implode(', ', array_keys($allowed))], 400);
|
||||
}
|
||||
|
||||
$data = readJsonFile($allowed[$type]);
|
||||
sendJson(['success' => true, 'type' => $type, 'data' => $data]);
|
||||
@ -17,4 +17,10 @@ APP_URL=http://localhost/family-hub
|
||||
|
||||
# Export Settings
|
||||
EXPORT_FREQUENCY=daily
|
||||
EXPORT_RETENTION_DAYS=30
|
||||
EXPORT_RETENTION_DAYS=30
|
||||
|
||||
# MCP / machine clients (optional). Leave MCP_API_TOKEN empty to disable Bearer auth.
|
||||
# Generate MCP_API_TOKEN with: openssl rand -hex 32
|
||||
MCP_API_TOKEN=
|
||||
# people.json id of the Head of household this token should act as (must be role head_of_household)
|
||||
MCP_ACTOR_PERSON_ID=
|
||||
@ -4,6 +4,7 @@ require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/utils.php';
|
||||
require_once __DIR__ . '/persona.php';
|
||||
require_once __DIR__ . '/mcp_token_auth.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
@ -11,6 +12,8 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
familyHubApplyMcpTokenAuthIfConfigured();
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
59
includes/mcp_token_auth.php
Normal file
59
includes/mcp_token_auth.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/persona.php';
|
||||
|
||||
/**
|
||||
* If MCP_API_TOKEN is set in .env and the request sends a matching Bearer token,
|
||||
* establish session as MCP_ACTOR_PERSON_ID (must be an existing Head of household).
|
||||
* Browser requests without Authorization are unchanged.
|
||||
*/
|
||||
function familyHubApplyMcpTokenAuthIfConfigured(): void {
|
||||
$configured = Env::get('MCP_API_TOKEN', '');
|
||||
if ($configured === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$header = familyHubAuthorizationHeader();
|
||||
if ($header === null || !preg_match('/^\s*Bearer\s+(\S+)\s*$/i', $header, $matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$presented = $matches[1];
|
||||
if (!hash_equals($configured, $presented)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actorId = trim((string) Env::get('MCP_ACTOR_PERSON_ID', ''));
|
||||
if ($actorId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$person = findPersonById($people, $actorId);
|
||||
if ($person === null || ($person['role'] ?? '') !== ROLE_HEAD) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionPerson($actorId, true);
|
||||
}
|
||||
|
||||
function familyHubAuthorizationHeader(): ?string {
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION']) && is_string($_SERVER['HTTP_AUTHORIZATION']) && $_SERVER['HTTP_AUTHORIZATION'] !== '') {
|
||||
return $_SERVER['HTTP_AUTHORIZATION'];
|
||||
}
|
||||
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && is_string($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] !== '') {
|
||||
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
|
||||
}
|
||||
if (function_exists('apache_request_headers')) {
|
||||
$headers = apache_request_headers();
|
||||
if (is_array($headers)) {
|
||||
foreach ($headers as $name => $value) {
|
||||
if (strcasecmp((string) $name, 'Authorization') === 0 && is_string($value) && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
381
mcp/familyhub/server.php
Normal file
381
mcp/familyhub/server.php
Normal file
@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/**
|
||||
* Family Hub MCP server (stdio, JSON-RPC). No Composer or Node.
|
||||
*
|
||||
* Env (set in Cursor MCP config):
|
||||
* FAMILYHUB_BASE_URL e.g. https://example.com/family-hub/api
|
||||
* FAMILYHUB_MCP_TOKEN same value as MCP_API_TOKEN in .env
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
const FAMILYHUB_MCP_PROTOCOL_VERSION = '2024-11-05';
|
||||
|
||||
/** @var list<string> */
|
||||
const FAMILYHUB_MCP_POST_ALLOWLIST = [
|
||||
'family_settings_save.php',
|
||||
'meal_ingredient_to_grocery.php',
|
||||
'meal_push_grocery.php',
|
||||
'meal_plan_set_slot.php',
|
||||
'meal_plan_set_week.php',
|
||||
'meal_delete.php',
|
||||
'meal_save.php',
|
||||
'grocery_item_create.php',
|
||||
'grocery_item_review.php',
|
||||
'grocery_item_delete.php',
|
||||
'grocery_item_purchase.php',
|
||||
'store_delete.php',
|
||||
'store_update.php',
|
||||
'store_create.php',
|
||||
'chore_save.php',
|
||||
'expense_create.php',
|
||||
'chore_review.php',
|
||||
'chore_submit.php',
|
||||
'chore_delete.php',
|
||||
'people_create_first.php',
|
||||
'people_update.php',
|
||||
'people_create.php',
|
||||
'people_reset_pin.php',
|
||||
'people_delete.php',
|
||||
'switch_person.php',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
const FAMILYHUB_MCP_SNAPSHOT_TYPES = [
|
||||
'people',
|
||||
'chores',
|
||||
'stores',
|
||||
'grocery_lists',
|
||||
'grocery_catalog',
|
||||
'meals',
|
||||
'meal_plans',
|
||||
'family_settings',
|
||||
'expenses',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array{base: string, token: string}
|
||||
*/
|
||||
function familyHubMcpEnv(): array {
|
||||
$base = rtrim((string) getenv('FAMILYHUB_BASE_URL'), '/');
|
||||
$token = (string) getenv('FAMILYHUB_MCP_TOKEN');
|
||||
return ['base' => $base, 'token' => $token];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'GET'|'POST' $method
|
||||
* @return array{ok: bool, status: int, body: string}
|
||||
*/
|
||||
function familyHubMcpHttp(string $method, string $pathAndQuery, ?array $jsonBody): array {
|
||||
$c = familyHubMcpEnv();
|
||||
if ($c['base'] === '' || $c['token'] === '') {
|
||||
return ['ok' => false, 'status' => 0, 'body' => 'Set FAMILYHUB_BASE_URL and FAMILYHUB_MCP_TOKEN in the MCP server environment.'];
|
||||
}
|
||||
$path = ltrim($pathAndQuery, '/');
|
||||
if ($path === '' || strpos($path, '..') !== false) {
|
||||
return ['ok' => false, 'status' => 0, 'body' => 'Invalid path.'];
|
||||
}
|
||||
$url = $c['base'] . '/' . $path;
|
||||
|
||||
$headerLines = ['Authorization: Bearer ' . $c['token']];
|
||||
$content = '';
|
||||
if ($method === 'POST' && $jsonBody !== null) {
|
||||
$headerLines[] = 'Content-Type: application/json; charset=utf-8';
|
||||
$enc = json_encode($jsonBody, JSON_UNESCAPED_UNICODE);
|
||||
$content = $enc !== false ? $enc : '{}';
|
||||
}
|
||||
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headerLines),
|
||||
'content' => $content,
|
||||
'timeout' => 120,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = @file_get_contents($url, false, $ctx);
|
||||
$status = 0;
|
||||
if (isset($http_response_header) && is_array($http_response_header)) {
|
||||
foreach ($http_response_header as $line) {
|
||||
if (preg_match('#^HTTP/\S+\s+(\d+)#', $line, $m)) {
|
||||
$status = (int) $m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($result === false) {
|
||||
return ['ok' => false, 'status' => $status, 'body' => 'HTTP request failed for ' . $url];
|
||||
}
|
||||
return ['ok' => $status >= 200 && $status < 300, 'status' => $status, 'body' => $result];
|
||||
}
|
||||
|
||||
function familyHubMcpWriteJsonrpc(array $payload): void {
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
return;
|
||||
}
|
||||
$out = 'Content-Length: ' . strlen($json) . "\r\n\r\n" . $json;
|
||||
fwrite(STDOUT, $out);
|
||||
fflush(STDOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
function familyHubMcpReadMessage(): ?array {
|
||||
$headerLines = [];
|
||||
while (true) {
|
||||
$line = fgets(STDIN);
|
||||
if ($line === false) {
|
||||
return null;
|
||||
}
|
||||
$line = rtrim($line, "\r\n");
|
||||
if ($line === '') {
|
||||
break;
|
||||
}
|
||||
$headerLines[] = $line;
|
||||
}
|
||||
$contentLength = 0;
|
||||
foreach ($headerLines as $hl) {
|
||||
if (stripos($hl, 'Content-Length:') === 0) {
|
||||
$contentLength = (int) trim(substr($hl, strlen('Content-Length:')));
|
||||
}
|
||||
}
|
||||
if ($contentLength <= 0) {
|
||||
return null;
|
||||
}
|
||||
$body = '';
|
||||
while (strlen($body) < $contentLength) {
|
||||
$chunk = fread(STDIN, $contentLength - strlen($body));
|
||||
if ($chunk === false || $chunk === '') {
|
||||
return null;
|
||||
}
|
||||
$body .= $chunk;
|
||||
}
|
||||
$decoded = json_decode($body, true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
function familyHubMcpToolList(): array {
|
||||
$typesEnum = ['type' => 'string', 'enum' => FAMILYHUB_MCP_SNAPSHOT_TYPES];
|
||||
$postEnum = ['type' => 'string', 'enum' => FAMILYHUB_MCP_POST_ALLOWLIST];
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'familyhub_health',
|
||||
'description' => 'Check MCP configuration and API reachability via family_settings snapshot.',
|
||||
'inputSchema' => ['type' => 'object', 'properties' => new stdClass()],
|
||||
],
|
||||
[
|
||||
'name' => 'familyhub_get_snapshot',
|
||||
'description' => 'Read allowlisted JSON domain data (people, chores, stores, grocery_lists, grocery_catalog, meals, meal_plans, family_settings, expenses).',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => ['type' => $typesEnum],
|
||||
'required' => ['type'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'familyhub_calendar_events',
|
||||
'description' => 'GET api/calendar_events.php — agenda for a date range. Omit from/to for server default window.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'from' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
|
||||
'to' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'familyhub_post_api',
|
||||
'description' => 'POST JSON to an allowlisted api/*.php endpoint (same contract as the web UI). Body is the JSON object sent as the POST body.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'endpoint' => $postEnum,
|
||||
'body' => ['type' => 'object', 'description' => 'JSON object payload'],
|
||||
],
|
||||
'required' => ['endpoint', 'body'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @return array{isError?: bool, content: list<array{type: string, text: string}>}
|
||||
*/
|
||||
function familyHubMcpRunTool(string $name, array $args): array {
|
||||
if ($name === 'familyhub_health') {
|
||||
$r = familyHubMcpHttp('GET', 'data_snapshot.php?type=family_settings', null);
|
||||
$text = $r['ok']
|
||||
? $r['body']
|
||||
: 'HTTP ' . $r['status'] . ': ' . $r['body'];
|
||||
return ['content' => [['type' => 'text', 'text' => $text], ], 'isError' => !$r['ok']];
|
||||
}
|
||||
if ($name === 'familyhub_get_snapshot') {
|
||||
$type = isset($args['type']) ? trim((string) $args['type']) : '';
|
||||
if (!in_array($type, FAMILYHUB_MCP_SNAPSHOT_TYPES, true)) {
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => 'type must be one of: ' . implode(', ', FAMILYHUB_MCP_SNAPSHOT_TYPES)]],
|
||||
'isError' => true,
|
||||
];
|
||||
}
|
||||
$r = familyHubMcpHttp('GET', 'data_snapshot.php?type=' . rawurlencode($type), null);
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => $r['body']]],
|
||||
'isError' => !$r['ok'],
|
||||
];
|
||||
}
|
||||
if ($name === 'familyhub_calendar_events') {
|
||||
$from = isset($args['from']) ? trim((string) $args['from']) : '';
|
||||
$to = isset($args['to']) ? trim((string) $args['to']) : '';
|
||||
$q = 'calendar_events.php';
|
||||
if ($from !== '' && $to !== '') {
|
||||
$q .= '?from=' . rawurlencode($from) . '&to=' . rawurlencode($to);
|
||||
}
|
||||
$r = familyHubMcpHttp('GET', $q, null);
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => $r['body']]],
|
||||
'isError' => !$r['ok'],
|
||||
];
|
||||
}
|
||||
if ($name === 'familyhub_post_api') {
|
||||
$endpoint = isset($args['endpoint']) ? trim((string) $args['endpoint']) : '';
|
||||
if (!in_array($endpoint, FAMILYHUB_MCP_POST_ALLOWLIST, true)) {
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => 'endpoint not allowlisted']],
|
||||
'isError' => true,
|
||||
];
|
||||
}
|
||||
$body = $args['body'] ?? [];
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
$r = familyHubMcpHttp('POST', $endpoint, $body);
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => $r['body']]],
|
||||
'isError' => !$r['ok'],
|
||||
];
|
||||
}
|
||||
return [
|
||||
'content' => [['type' => 'text', 'text' => 'Unknown tool']],
|
||||
'isError' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $msg
|
||||
*/
|
||||
function familyHubMcpHandleMessage(array $msg): void {
|
||||
$method = isset($msg['method']) ? (string) $msg['method'] : '';
|
||||
$isNotification = !array_key_exists('id', $msg);
|
||||
$id = !$isNotification ? $msg['id'] : null;
|
||||
|
||||
if ($method === 'notifications/initialized' || $method === 'notifications/cancelled') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'initialize') {
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => [
|
||||
'protocolVersion' => FAMILYHUB_MCP_PROTOCOL_VERSION,
|
||||
'capabilities' => ['tools' => new stdClass()],
|
||||
'serverInfo' => ['name' => 'familyhub', 'version' => '1.0.0'],
|
||||
],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'ping') {
|
||||
if (!$isNotification) {
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => new stdClass(),
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'tools/list') {
|
||||
if (!$isNotification) {
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => ['tools' => familyHubMcpToolList()],
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'resources/list') {
|
||||
if (!$isNotification) {
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => ['resources' => []],
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'prompts/list') {
|
||||
if (!$isNotification) {
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => ['prompts' => []],
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'tools/call') {
|
||||
if ($isNotification) {
|
||||
return;
|
||||
}
|
||||
$params = $msg['params'] ?? [];
|
||||
if (!is_array($params)) {
|
||||
$params = [];
|
||||
}
|
||||
$toolName = isset($params['name']) ? (string) $params['name'] : '';
|
||||
$arguments = $params['arguments'] ?? [];
|
||||
if (!is_array($arguments)) {
|
||||
$arguments = [];
|
||||
}
|
||||
$run = familyHubMcpRunTool($toolName, $arguments);
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'result' => $run,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
familyHubMcpWriteJsonrpc([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'error' => ['code' => -32601, 'message' => 'Method not found: ' . $method],
|
||||
]);
|
||||
}
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "familyhub MCP server: run from CLI.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
while (($msg = familyHubMcpReadMessage()) !== null) {
|
||||
familyHubMcpHandleMessage($msg);
|
||||
}
|
||||
31
readme.md
31
readme.md
@ -72,6 +72,36 @@ The **Calendar** tab embeds your family calendar in the hub using these values (
|
||||
| **`EXPORT_FREQUENCY`** | How often exports are expected to run | Your choice (e.g. `daily`). Match how you schedule **`scripts/daily_export.php`** in cron. |
|
||||
| **`EXPORT_RETENTION_DAYS`** | How long to keep export files (interpreted by app logic) | Integer number of days (e.g. `30`). |
|
||||
|
||||
### MCP / AI automation (optional)
|
||||
|
||||
These enable **machine-to-machine** access to the same JSON APIs the browser uses. When `MCP_API_TOKEN` is non-empty, API requests may send `Authorization: Bearer <MCP_API_TOKEN>`; the app then treats the request as the Head of household identified by `MCP_ACTOR_PERSON_ID` (PIN-verified semantics), without a browser session.
|
||||
|
||||
| Variable | What it is | Where to find it |
|
||||
| -------- | ----------- | ---------------- |
|
||||
| **`MCP_API_TOKEN`** | Shared secret for automation (Cursor MCP, scripts, etc.) | Generate locally (treat like a password). Example: `openssl rand -hex 32`. Paste into `.env`. Never commit the real value. If left **empty**, Bearer impersonation is disabled and only normal browser sessions work. |
|
||||
| **`MCP_ACTOR_PERSON_ID`** | Which person the token impersonates | Open **`data/people.json`** (after setup) and copy the **`id`** field for your **Head of household** profile. Must match a person whose **`role`** is `head_of_household`. Wrong or non-head IDs are ignored and the token will not authenticate. |
|
||||
|
||||
**Security:** The token grants the same power as a logged-in, HoH-verified session (create chores, manage people, grocery data, etc.). Use **HTTPS** in production. Rotate the token by changing `.env` and restarting anything that caches env.
|
||||
|
||||
**Cursor MCP:** The MCP server is plain PHP (no Node): **`mcp/familyhub/server.php`**. Register it in Cursor **Settings → MCP** with the absolute path to PHP and the script, and set environment variables `FAMILYHUB_BASE_URL` (your hub’s **`…/api`** URL, e.g. `http://localhost/family-hub/api`) and `FAMILYHUB_MCP_TOKEN` (same value as **`MCP_API_TOKEN`**). Tools exposed: `familyhub_health`, `familyhub_get_snapshot`, `familyhub_calendar_events`, `familyhub_post_api` (allowlisted POST endpoints only).
|
||||
|
||||
Example `mcp.json` server entry (adjust paths and env):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"familyhub": {
|
||||
"command": "php",
|
||||
"args": ["/full/path/to/familyHub/mcp/familyhub/server.php"],
|
||||
"env": {
|
||||
"FAMILYHUB_BASE_URL": "http://localhost/family-hub/api",
|
||||
"FAMILYHUB_MCP_TOKEN": "paste-the-same-secret-as-MCP_API_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Calendar preferences (family settings)
|
||||
|
||||
These are **not** in `.env`; they live in `data/family_settings.json` and are edited under **Family settings → Calendar and dates**.
|
||||
@ -116,6 +146,7 @@ familyHub/
|
||||
├── exports/ # Export output (not tracked in git)
|
||||
├── scripts/ # Cron / CLI helpers
|
||||
│ └── daily_export.php
|
||||
├── mcp/familyhub/ # Optional MCP stdio server (PHP only): `server.php`
|
||||
├── tabs/ # Tab views included by `index.php`
|
||||
│ ├── chores.php
|
||||
│ ├── groceries.php
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user