# Claude + MCP Integration — Feature Documentation

> **آخر تحديث:** 2026-06-09
> **الـ Stack:** Laravel 12 / PHP 8.2 / MySQL 8 / Apache 2.4
> **الـ Spec:** OAuth 2.1 + MCP Authorization 2026 draft + RFC 9728 + RFC 8414

---

## 1. الفكرة بإيجاز

الفيتشر دي بتخلي **Claude.ai (Web و Desktop)** يتصل بـ Numu كأداة شغل — يبحث، يعدّل، يثري بيانات المستثمرين والشركات الناشئة من خلال natural language. الأدمن يكتب في Claude:

> "اعرضلي اول 20 مستثمر في group Angel Investors"

Claude بترجم الطلب لـ MCP tool call، السيرفر يفلتر ويرجّع النتيجة. كل عملية تعديل بتسجل في audit log قابل للـ rollback.

**مفيش approval queue** — كل تعديل بيتطبق فوريًا، والـ rollback هو وسيلة التحكم.

---

## 2. اللي بنيناه — العشرة طبقات

```
┌───────────────────────────────────────────────────────────────────┐
│  1.  Claude Web / Claude Desktop / Claude Code / Cursor           │
└──────────────────────────┬────────────────────────────────────────┘
                           │  HTTPS
                           ▼
┌───────────────────────────────────────────────────────────────────┐
│  2.  OAuth 2.1 Authorization Server                               │
│      • /.well-known/oauth-authorization-server  (RFC 8414)        │
│      • /.well-known/oauth-protected-resource    (RFC 9728)        │
│      • /oauth/authorize  (PKCE-only, S256)                        │
│      • /oauth/token      (auth-code + refresh-token grants)       │
│      • /oauth/revoke     (RFC 7009)                               │
│      • /oauth/introspect (RFC 7662)                               │
│      • /oauth/userinfo   (OIDC-style)                             │
│      • Auto-CIMD registration للـ claude.ai (RFC draft)           │
└──────────────────────────┬────────────────────────────────────────┘
                           │  Bearer token (naat_*)
                           ▼
┌───────────────────────────────────────────────────────────────────┐
│  3.  McpAuth Middleware  (validates token + scope hierarchy)      │
└──────────────────────────┬────────────────────────────────────────┘
                           │
            ┌──────────────┴──────────────┐
            ▼                             ▼
┌─────────────────────┐         ┌─────────────────────┐
│ 4. /api/v1/ai/mcp/  │         │ 5. /api/v1/         │
│      Streamable     │         │      investors      │
│      HTTP MCP       │         │      REST endpoint  │
│      (read + full)  │         │                     │
└─────────┬───────────┘         └─────────┬───────────┘
          │                               │
          ▼                               ▼
┌─────────────────────┐         ┌─────────────────────┐
│ 6. McpToolRegistry  │         │ 7. InvestorApi      │
│    34 tools across  │         │    Controller       │
│    8 entities       │         │    (delegates to)   │
└─────────┬───────────┘         └─────────┬───────────┘
          │                               │
          ▼                               ▼
┌──────────────────────────────────────────────────┐
│  8.  Domain Services + Write Controllers          │
│      • AiInvestorWriteController                  │
│      • InvestorApiWriteController                 │
│      • InvestorQueryBuilder (filter brain)        │
│      • EnrichmentEngine                           │
│      • AssetUploader                              │
└──────────────────────┬───────────────────────────┘
                       │  DB::transaction
                       ▼
┌──────────────────────────────────────────────────┐
│  9.  Atomic Write (entity + audit row, single tx)│
└──────────────────────┬───────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│ 10.  ai_activity_logs  → AiRollbackService       │
│      Append-only audit log + per-row / session / │
│      task / range rollback                        │
└──────────────────────────────────────────────────┘
```

---

## 3. الـ OAuth 2.1 Layer — تفاصيل كاملة

### 3.1 الـ flow الكامل لما الأدمن يضيف connector

```
1.  الأدمن في Claude Desktop:
    Settings → Connectors → Add custom connector
    URL: https://dashboard.numuangels.net/api/v1/ai/mcp/full

2.  Claude بتعمل POST /api/v1/ai/mcp/full بدون token
    ↓
    السيرفر يرد: 401 + WWW-Authenticate header

        WWW-Authenticate: Bearer realm="Numu",
                          error="missing_token",
                          scope="numu:write",
                          resource_metadata="https://dashboard.numuangels.net/.well-known/oauth-protected-resource"

3.  Claude بتقرأ resource_metadata و تعمل:
    GET /.well-known/oauth-protected-resource   → JSON بيقول وين الـ AS
    GET /.well-known/oauth-authorization-server → endpoints + scopes_supported

4.  Claude بتفتح متصفح للأدمن على:
    /oauth/authorize?
        response_type=code
        &client_id=https://claude.ai/oauth/mcp-oauth-client-metadata  ← CIMD URL!
        &redirect_uri=https://claude.ai/api/mcp/auth_callback
        &scope=numu:write
        &code_challenge=<base64url SHA256>
        &code_challenge_method=S256
        &state=<csrf>
        &resource=https://dashboard.numuangels.net/api/v1/ai/mcp/full

5.  AuthorizeController::show()
    a) ClientResolver يلاقي إن client_id مش مسجّل
    b) maybeAutoRegisterCimd() يشوف إن الـ host (claude.ai) في
       trusted list → يـ fetch /oauth/mcp-oauth-client-metadata
    c) Validates the metadata: HTTPS only, valid JSON, redirect_uris[]
    d) Creates OauthClient row + caches metadata (1 hour TTL)
    e) AdminJwtAuthSoft middleware decodes the admin JWT cookie if present

6.  لو الأدمن logged in → consent page تظهر مباشرة
    لو مش logged in → redirect to /admin/login?continue=<original-URL>

7.  بعد الـ consent (موافق):
    a) Code يتمنته (60s TTL, single-use, hashed in DB)
    b) Redirect → claude.ai/api/mcp/auth_callback?code=nac_xxx&state=xxx&iss=...

8.  Claude بـ exchange الـ code:
    POST /oauth/token
        grant_type=authorization_code
        code=nac_xxx
        code_verifier=<base64url>     ← PKCE
        client_id=<CIMD URL>
        redirect_uri=<must exact match>
        resource=<must exact match>

9.  TokenController::exchangeCode()
    a) Atomic markCodeUsed (race-condition-proof)
    b) PKCE verify (S256, constant-time hash_equals)
    c) Issues access_token (naat_xxx, 15-min TTL) + refresh_token (nart_xxx, 30-day sliding)
    d) Both stored as sha256 hashes; family_id links them for rotation

10. Connected! Claude بتـ store الـ tokens وتبدأ تنده tools.
```

### 3.2 الـ Refresh Token Rotation

كل refresh بيـ rotate الـ token:

```
POST /oauth/token
    grant_type=refresh_token
    refresh_token=nart_OLD
    client_id=<CIMD URL>
    resource=<exact match>

→ Atomic `UPDATE oauth_refresh_tokens SET used_at = NOW() WHERE used_at IS NULL`
→ لو الـ UPDATE أثّر على 0 صفوف:
   → الـ token مستخدم من قبل (reuse detection!)
   → revoke كل الـ family (الـ chain كاملة)
   → audit row بـ outcome=blocked + reason=refresh_reused
   → Claude يضطر يعمل OAuth flow كامل من الأول
→ لو الـ UPDATE أثّر على 1 صف:
   → mint refresh_token جديد بنفس family_id
   → mint access_token جديد
   → Return both
```

ده آلية الكشف عن الـ stolen refresh tokens.

### 3.3 الـ Scope Hierarchy

```
numu:admin   ⊇  numu:read + numu:write + numu:enrich
numu:enrich  ⊇  numu:read + numu:write
numu:write   ⊇  numu:read
```

الـ expansion بيحصل في `McpAuth::expandScopeHierarchy()`. لما Claude تطلب `scope=numu:write` بس، السيرفر بيوسّع تلقائيًا لـ `[numu:read, numu:write]` عشان tools الـ read تشتغل.

---

## 4. الـ MCP Server — Streamable HTTP

### 4.1 الـ Transport

طبقًا للـ MCP spec 2026 draft، الـ endpoints:

| Route | Method | الاستخدام |
|---|---|---|
| `/api/v1/ai/mcp/read` | POST | JSON-RPC calls على read tools (18 tool) |
| `/api/v1/ai/mcp/full` | POST | JSON-RPC calls على all tools (34 tool) |
| `/api/v1/ai/mcp/full` | GET | 405 + Allow header (مفيش server-side streaming) |
| `/api/v1/ai/mcp/full` | DELETE | 200 (session termination ack) |

### 4.2 الـ Content Negotiation

```
POST /api/v1/ai/mcp/full
Accept: application/json
   → الرد JSON عادي

POST /api/v1/ai/mcp/full
Accept: text/event-stream
   → الرد SSE: "event: message\ndata: {...}\n\n"
```

### 4.3 Mcp-Session-Id

```
Initialize request → سيرفر بيمنته UUID جديد
   → بيرجع في الـ Mcp-Session-Id header

Subsequent requests → Claude بتبعت نفس الـ Mcp-Session-Id
   → السيرفر بيـ echo-back
```

### 4.4 Batched JSON-RPC

```
POST /api/v1/ai/mcp/full
[
  {"jsonrpc":"2.0","id":1,"method":"tools/call",...},
  {"jsonrpc":"2.0","id":2,"method":"tools/call",...}
]
   → الرد: array of results
```

---

## 5. الـ Tool Registry

### 5.1 شكل الـ tool

```php
[
    'name'        => 'investor_list',
    'tier'        => 'read',       // أو 'full'
    'title'       => '...',
    'description' => '...',         // Claude بتقرأها لتختار الأداة
    'schema'      => [              // JSON Schema للـ arguments
        'type'       => 'object',
        'properties' => [ ... ],
    ],
    'handler' => function (array $a, Request $r) {
        // الـ logic
        return [...];
    },
],
```

### 5.2 الـ 34 tools (مقسومة على tiers)

#### Read tier (18 tool — متاح بـ `numu:read`)

| Tool | الغرض |
|---|---|
| `investor_list` | listing + filtering (mirror of admin dashboard) |
| `investor_search` | text search عام |
| `investor_get` | full record بـ notes + tags + files + gaps |
| `investor_find_gaps` | detect data quality gaps |
| `investor_filter_dictionary` | discovery: list groups + tags + label options |
| `startup_list`, `startup_search`, `startup_get`, `startup_find_gaps` | same for startups |
| `committee_list`, `committee_get` | committees |
| `tag_list` | كل الـ tags في النظام |
| `label_option_list` | options لأي label key |
| `note_list_for` | notes على entity |
| `meeting_list_for` | meetings على entity |
| `task_list`, `task_summary` | AI tasks |

#### Full tier (16 tool إضافية — تطلب `numu:write` أو أعلى)

| Tool | الغرض |
|---|---|
| `investor_update` | تعديل مستثمر (whitelisted fields) |
| `investor_bulk_update` | تعديل مجموعة (max 500) |
| `investor_upload_avatar` | رفع صورة من URL |
| `startup_update`, `startup_bulk_update`, `startup_upload_logo` | same for startups |
| `tag_create`, `tag_attach_investor`, `tag_detach_investor` | tag management |
| `note_create`, `note_update` | notes management |
| `meeting_create` | جدولة meeting |
| `task_start`, `task_resume` | بدء/استئناف AI tasks |
| `enrichment_run`, `enrichment_preview` | الـ enrichment engine |
| `enrichment_preview_proposals_*` (4 tools) | preview approval (write/admin scope) |

### 5.3 Per-tool scope enforcement

```php
// في AiMcpController::onCallTool():
$requiredScope = $this->registry->requiredScopeFor($name);
$grantedScopes = (array) $request->attributes->get('granted_scopes', []);

if (!in_array($requiredScope, $grantedScopes, true)) {
    return $this->toolErrorBody($id,
        "Tool '{$name}' requires scope '{$requiredScope}', not granted by your token.",
        [...]
    );
}
```

الـ enrichment_preview_proposals_{approve,reject,bulk_approve} مغلّظة بـ `numu:admin` (SCOPE_OVERRIDES في `McpToolRegistry`).

---

## 6. الـ REST Endpoint — `/api/v1/investors`

### 6.1 الـ Surface

| Method | Path | Auth | Tier |
|---|---|---|---|
| GET | `/api/v1/investors` | Bearer (OAuth or Sanctum) | numu:read |
| PATCH | `/api/v1/investors/{id}` | Bearer | numu:write |

### 6.2 الـ Filter Surface (35+ filter)

كل dashboard filter متاح + filters إضافية:

```
?q=                  Global search (name, first/last, email, phone)
?id[]=               Multi-id lookup
?group=Angel Investors        ← اسم الـ group (case-insensitive)
?group_id=11                   ← ID مباشر
?group_id[]=11&group_id[]=12  ← multi-group
?category=C-Committee          ← اسم
?category_id=167               ← ID
?category_option_id=167        ← Dashboard URL form
?action=Reject                 ← اسم (bilingual ar+en)
?action_id=171
?tag=VIP                       ← اسم single tag
?tags[]=5&tags[]=8             ← multi-tag IDs
?tags_mode=all                 ← AND mode (default OR)
?phone=01069195365             ← partial match
?phone_exact=...               ← byte-exact
?email=, ?email_exact=
?status=active|inactive|archived|any
?value_min=0&value_max=10000   ← investment_cap_sar range
?investments_count_min/max
?expected_yearly_min/max
?created_from=..&created_to=..
?created_between=2026-01-01,2026-12-31
?updated_between=...
?duplicates_only=1, ?favorites=1
?meeting_status=booked|never_booked
?country_id=, ?city_id=
?sort=created_at&dir=desc
?per_page=50&page=1            ← max per_page=200
?cache=1                       ← opt-in 30s cache
```

### 6.3 الـ Response Shape

```json
{
  "data": [
    {
      "id": 614,
      "basic": {
        "name": "...",
        "first_name", "last_name",
        "first_name_ar", "first_name_en",
        "full_name_ar", "full_name_en",
        "email", "phone_number",
        "linkedin_profile",
        "avatar_path", "avatar_url"
      },
      "profile": {
        "bio",
        "country_id", "country": {id, iso2, name_en, name_ar},
        "city_id", "city": {...}
      },
      "financials": {
        "investments_count",
        "expected_investment_per_year_sar",
        "investment_cap_sar",
        "previous_investments_companies": [...]
      },
      "lifecycle": {
        "is_active", "is_duplicate", "is_favorite", "is_archived",
        "joining_date", "demo_date",
        "rejection_reason",
        "whatsapp_group_status",
        "action": {id, name_en, name_ar},     ← resolved label
        "category": {id, name_en, name_ar},
        "average_ticket": {...},
        ... 8 more label options
      },
      "tags": [{id, name, color}],
      "group": {id, name},
      "activity_summary": {
        "has_notes", "note_count", "last_note_at",
        "has_meetings", "meeting_count", "last_meeting_at",
        "has_files", "file_count"
      },
      "display_names": {                      ← flat AI-friendly
        "group": "Angel Investors",
        "category": "C-Committee",
        "action": "Reject",
        "tags": ["VIP", ...]
      },
      "metadata": {
        "source", "created_at", "updated_at",
        "monday_item_id"
      }
    }
  ],
  "meta": {
    "page", "per_page", "total", "last_page", "has_more",
    "applied_filters": {...},
    "unsupported_filters": [...],
    "sort": {column, dir},
    "cache": {hit, key},
    "doc": {...}
  }
}
```

### 6.4 PATCH — Write Surface

38 حقل writable (mirrors the read shape). شكل الـ request:

```json
PATCH /api/v1/investors/614
{
  "name": "محمد سعد علم الدين",
  "full_name_en": "Mohamed Saad",
  "whatsapp_group_status": "added",
  "country_id": 17,
  "category_option_id": 167,
  "tags": [3, 7, 12],            ← يستبدل الـ tag set كله
  "rationale": "post-onboarding cleanup"
}
```

Per-field validation rules في `InvestorApiWriteController::fieldRules()`. كل write atomic مع audit log.

---

## 6.5 الـ REST Endpoint — `/api/v1/startups` (Startup parity)

> **آخر تحديث:** 2026-06-09 — تم بناء parity كاملة مع الـ investors بفلسفة "MCP-first".

### 6.5.1 المبدأ التصميمي

نفس فلسفة `/api/v1/investors`، لكن مع تخصيصات لـ schema الـ startups:
- 12 label dimensions (vs 11 للـ investors)
- مفيش tags relation
- فيه team (members) + geographic_focuses sections
- الـ logo عبر `files` table polymorphic (مش column مباشر)
- اسم `status` غامض — `?status=` للـ lifecycle، `?status_label=` للـ label option

**المتطلب الإلزامي:** كل row في الـ list response **يجب** يحتوي `display_names` populated. الـ AI بـ يـ reason ويـ filter من الـ list مباشرة بدون per-row `startup_get`.

### 6.5.2 الـ Surface

| Method | Path | Auth | Tier |
|---|---|---|---|
| GET | `/api/v1/startups` | Bearer (OAuth + Sanctum) | `numu:read` |
| PATCH | `/api/v1/startups/{id}` | Bearer | `numu:write` |

### 6.5.3 الـ Filter Surface (40+ filter)

```
# Identity + global search
?q=                      ← يبحث في 7 columns:
                            name, full_name, first_name_ar, first_name_en,
                            email, phone_number, website
?id[]=                   ← multi-id

# Name-based filters (المسار المفضّل للـ AI prompts)
?group=Pipeline                     ← groups.name (case-insensitive)
?sector=Fintech                     ← label_options "sector" (bilingual)
?action=Approve                     ← label_options "action"
?status_label=Active                ← label_options "status" (مختلف عن ?status= !)
?product_stage=MVP                  ← label_options "product_stage"
?investment_stage=Seed              ← label_options "investment_stage"
?traction=Pre-Revenue
?round_type=Pre-Seed
?customer_focus=B2B
?revenue_model=SaaS
?previously_received_funding=Yes
?referral_source=Entrepreneur

# Geography by name
?country_name=Saudi                 ← countries.name_en / name_ar
?city_name=Riyadh                   ← cities.name_en / name_ar

# Contact (partial + exact)
?phone=, ?phone_exact=
?email=, ?email_exact=
?website=                           ← partial URL match

# Lifecycle (default: active)
?status=active|inactive|archived|any

# Financial ranges (numeric)
?asking_min=, ?asking_max=                 → asking_fund_sar
?raised_min=, ?raised_max=                 → total_raised_sar
?previous_funding_min=, ?previous_funding_max=  → previous_funding_amount_sar
?ticket_min=, ?ticket_max=                 → min_ticket_size_sar
?equity_min=, ?equity_max=                 → offered_equity_pct (0..100)
?runway_min=, ?runway_max=                 → runway_months

# Date ranges (3 input shapes)
?created_from=, ?created_to=
?created_between=YYYY-MM-DD,YYYY-MM-DD
?updated_between=...

# Flags
?duplicates_only=1, ?favorites=1

# FK shortcuts (when ID is known)
?country_id=, ?city_id=, ?group_id=
?sector_id=, ?action_id=, ?stage_id=        ← aliases for *_option_id
?sector_option_id[]=, ?action_option_id[]=  ← canonical (dashboard parity)

# Sort + page
?sort=, ?dir=asc|desc
?per_page= (max 200), ?page=
?cache=1                            ← opt-in 30s cache
```

### 6.5.4 شكل الـ Response (StartupResource)

```json
{
  "id": 42,
  "basic": {
    "name", "full_name", "first_name_ar", "first_name_en",
    "email", "phone_number",
    "website", "linkedin_url", "ios_url", "android_url",
    "logo_url"               // ← من files table (slot='logo')
  },
  "profile": {
    "startup_brief", "problem", "solution",
    "business_model", "investment_opportunity", "kpi_targets",
    "country_id", "country": {id, iso2, name_en, name_ar},
    "city_id", "city": {...}
  },
  "financials": {
    "asking_fund_sar", "total_raised_sar", "previous_funding_amount_sar",
    "min_ticket_size_sar", "offered_equity_pct", "runway_months",
    "valuation", "market_size_tam", "market_size_sam", "market_size_som",
    "fund_use": {operation_sar, marketing_sar, development_sar, salaries_sar},
    "cap_and_floor"
  },
  "lifecycle": {
    "is_active", "is_duplicate", "is_favorite", "is_archived",
    "rejection_reason",
    "action":            {id, name_en, name_ar},
    "status_label":      {id, name_en, name_ar},
    "sector":            {id, name_en, name_ar},
    "traction":          {...},
    "product_stage":     {...},
    "investment_stage":  {...},
    "round_type":        {...},
    "customer_focus":    {...},
    "revenue_model":     {...},
    "previously_received_funding": {...},
    "referral_source":   {...}
  },
  "group": {id, name},

  // Heavy — للـ get فقط (lazy via whenLoaded)
  "team": { "member_count": 3, "members": [...] },
  "geographic_focuses": [...],

  "activity_summary": {
    "has_notes", "note_count", "last_note_at",
    "has_meetings", "meeting_count", "last_meeting_at",
    "has_files", "file_count",
    "has_members", "member_count"
  },

  // ── MANDATORY — flat AI-friendly shortcut ──
  "display_names": {
    "name":              "Numu Tech",
    "full_name":         "Numu Tech LLC",
    "group":             "Pipeline",
    "sector":            "Fintech",
    "action":            "Approve",
    "status_label":      "Active",
    "product_stage":     "MVP",
    "investment_stage":  "Seed",
    "traction":          "Pre-Revenue",
    "round_type":        "Pre-Seed",
    "customer_focus":    "B2B",
    "revenue_model":     "SaaS",
    "previously_received_funding": "Yes",
    "referral_source":   "Entrepreneur",
    "country":           "Saudi Arabia",
    "city":              "Riyadh"
  },

  "metadata": {
    "source", "created_at", "updated_at",
    "last_synced_at", "deleted_at",
    "monday_item_id",
    "accepted_privacy_policy",
    "accepted_privacy_policy_at",
    "accepted_privacy_policy_version"
  }
}
```

### 6.5.5 PATCH — Write Surface مع Name Resolution

الـ headline feature: **يقبل أسماء بدل IDs**.

```json
PATCH /api/v1/startups/42
{
  "name": "Numu Tech Updated",
  "email": "new@example.com",

  // Name-based shortcuts (الـ controller يـ resolve لـ FK ids):
  "group": "Pipeline",                  ← group_id = 3
  "sector": "Fintech",                  ← sector_option_id = 22
  "action": "Approve",                  ← action_option_id
  "investment_stage": "Seed",
  "customer_focus": "B2B",
  "revenue_model": "SaaS",

  // OR raw IDs (لو معروفة):
  "country_id": 17,
  "action_option_id": 175,

  "rationale": "post-meeting decision"
}
```

**شكل الـ response:**

```json
{
  "data": { ... كامل الـ startup بالـ shape فوق ... },
  "meta": {
    "updated_fields":    ["name", "email", "group_id", "sector_option_id", ...],
    "name_resolved":     {"group_id": 3, "sector_option_id": 22},
    "unresolved":        {"action": "Approve (no match in label_options)"},
    "ignored_fields":    ["unknown_key"],
    "audit_row_id":      158
  }
}
```

### 6.5.6 الـ Resolution Strategy

1. **Pass-through:** لو `{"group": {"id": 11}}` → الـ id يُستخدم كما هو
2. **Exact match (case-insensitive)** على `name_en` أو `name_ar` → winner
3. **Partial match** بـ LIKE → لو واحد فقط، winner
4. **Multi-match:** يرجع `error: ambiguous_match` مع `candidates`
5. **No match:** يدخل في `meta.unresolved` مع توضيح الجدول

**الـ groups** filter بـ `type='startup'` — مفيش collision مع Investor groups بنفس الاسم.
**الـ label_options** filter بـ `labels.type='startup' AND labels.key=...` — مفيش collision مع Investor labels.

### 6.5.7 MCP Tools

```
🔄 startup_list                    ← REPLACED — يـ delegate لـ StartupApiController
                                     (كان trimmed cardShape، دلوقتي rich)
🆕 startup_filter_dictionary       ← مثل investor_filter_dictionary بالظبط
🔄 startup_update                  ← ENHANCED — يقبل `find_by` (name/email/phone/
                                     website/linkedin_url) + name resolution في changes
✅ startup_search, startup_get,
   startup_find_gaps                ← يفضلوا زي ما هم
```

### 6.5.8 شكل `startup_update` MCP tool

```json
{
  // Identify by id OR by find_by
  "id": 42,                              // OR:
  "find_by": {
    "name": "Numu Tech",                 // أو email/phone/website/linkedin_url
  },

  "changes": {
    "group": "Pipeline",                 // resolved
    "sector": "Fintech",                 // resolved
    "action": "Approve",                 // resolved
    "email": "new@example.com",          // direct
    "asking_fund_sar": 500000,           // direct
    "is_favorite": true                  // direct
  },

  "rationale": "post-pitch evaluation"
}
```

الـ handler يـ resolve الـ subject أولاً، ثم يـ delegate لـ REST controller اللي يـ resolve الأسماء في `changes`.

### 6.5.9 الـ Discovery — `startup_filter_dictionary`

**استدعاء واحد** يرجع كل المفردات المسموحة:

```json
{
  "groups": [
    {"id": 1, "name": "new",      "startup_count": 14},
    {"id": 3, "name": "Pipeline", "startup_count": 2},
    ...
  ],
  "label_options": {
    "sector":            [{id, name_en, name_ar, startup_count}, ...],
    "action":            [...],
    "status_label":      [...],
    "product_stage":     [...],
    "investment_stage":  [...],
    "traction":          [...],
    "round_type":        [...],
    "customer_focus":    [...],
    "revenue_model":     [...],
    "previously_received_funding": [...],
    "referral_source":   [...]
  },
  "meta": {
    "kind": "all",
    "total_groups": 10,
    "total_label_options": 78,
    "usage_hint": "Pass any 'name' value back to startup_list..."
  }
}
```

**Scope-only:** `?kind=sector` يرجع الـ sectors فقط (lightweight).

### 6.5.10 الـ Claude Flow الجديد

```
المستخدم: "حرّك Numu Tech للـ Pipeline group وعدل action لـ Approve"
   ↓
Claude يـ call: startup_filter_dictionary(kind="all")
   ↓
يشوف:  groups[].name include "Pipeline" + label_options.action include "Approve"
   ↓
Claude يـ call: startup_update({
    find_by: {name: "Numu Tech"},
    changes: {
        group: "Pipeline",
        action: "Approve"
    }
})
   ↓
السيرفر:
    1. resolveStartupForUpdate → يلاقي #42 بـ exact match
    2. StartupApiWriteController يـ resolve names:
         group → groups WHERE type='startup' AND name LIKE 'Pipeline' → id=3
         action → label_options WHERE labels.key='action' AND name LIKE 'Approve' → id=175
    3. atomic UPDATE startups SET group_id=3, action_option_id=175 WHERE id=42
    4. audit row في ai_activity_logs
   ↓
Response:
    {
      data: { ... full updated startup ... },
      meta: {
        updated_fields: ["group_id", "action_option_id"],
        name_resolved:  {"group_id": 3, "action_option_id": 175}
      }
    }
```

### 6.5.11 الأمثلة اللي بـ تشتغل من برومت طبيعي

| البرومت | الـ tool call | النتيجة |
|---|---|---|
| "الشركات في Fintech sector" | `startup_list({sector: "Fintech"})` | 62 startup |
| "شركات في stage MVP" | `startup_list({product_stage: "MVP"})` | match |
| "شركات بتطلب فوق 100K SAR" | `startup_list({asking_min: 100000})` | 381 |
| "الشركات الـ B2B SaaS" | `startup_list({customer_focus: "B2B", revenue_model: "SaaS"})` | combo |
| "Pipeline group" | `startup_list({group: "Pipeline"})` | 2 |
| "اعرض شركة Numu Tech" | `startup_search({q: "Numu Tech"})` | match |
| "موّع Numu Tech للـ Pipeline" | `startup_update({find_by: {name: "Numu"}, changes: {group: "Pipeline"}})` | resolved |
| "غيّر sector شركة #42 لـ Healthtech" | `startup_update({id: 42, changes: {sector: "Healthtech"}})` | resolved |

### 6.5.12 الـ Read-only Fields (NOT writable via PATCH)

```
❌ id, created_at, updated_at, deleted_at, last_synced_at
❌ logo_url (محسوب من files)
❌ is_archived (محسوب من deleted_at)
❌ accepted_privacy_policy* (سجل قانوني — يجي من signup)
❌ monday_* (sync-owned — تعديلها يكسر الـ sync)
❌ source (managed by signup/sync flow)
❌ canonical_id (duplicate-merge infrastructure)
❌ note_type_option_id (admin-only workflow)
```

### 6.5.13 الـ Files المعمولة

```
🆕 app/Services/Startups/StartupQueryBuilder.php
🆕 app/Http/Resources/StartupResource.php
🆕 app/Http/Controllers/Api/V1/StartupApiController.php
🆕 app/Http/Controllers/Api/V1/StartupApiWriteController.php

🔄 app/Services/Ai/Mcp/McpToolRegistry.php
   - REPLACED startup_list handler + schema (rich, full parity)
   - ADDED startup_filter_dictionary tool + helpers
   - ENHANCED startup_update (find_by + name resolution)

🔄 routes/api.php
   - + GET    /api/v1/startups
   - + PATCH  /api/v1/startups/{startup}
```

### 6.5.14 الفروق المهمة عن `/api/v1/investors`

| Feature | Investors | Startups |
|---|---|---|
| Label count | 11 | 12 |
| Multi-tag pivot | ✅ `tags[]=` AND/OR | ❌ مفيش tags relation |
| Team data | — | ✅ `members` + `positions` (heavy, get-only) |
| Locations multi-select | ❌ | ✅ `geographic_focuses` (heavy, get-only) |
| Logo storage | column `avatar_path` | `files` table (slot='logo') |
| `status` field collision | — | ⚠️ `?status=` لـ lifecycle، `?status_label=` لـ label |
| Find by name resolver | Not implemented in PATCH | ✅ `find_by: {name, email, phone, website, linkedin_url}` |

---

## 7. الـ Middleware Stack

| Middleware | الغرض | على وين |
|---|---|---|
| `McpAuth` | Validate Bearer (OAuth `naat_*` أو Sanctum) + audience + scope + admin lookup | `/api/v1/ai/mcp/*`, `/api/v1/investors` |
| `OAuthBearerAuth` | Same but for AS endpoints (userinfo) | `/oauth/userinfo` |
| `AdminJwtAuth` | Strict — decode JWT cookie, redirect to login if absent | `/admin/*` |
| `AdminJwtAuthSoft` | Soft — decode if present, no redirect | `/oauth/authorize` |
| `admin.guest` | Bounce logged-in admins to dashboard | `/admin/login` |
| `throttle:investors-api` | 60/min per token + 120/min per user | `/api/v1/investors` |
| `throttle:30,1` | 30/min per IP | `/oauth/authorize`, `/oauth/authorize/decide` |
| `throttle:60,1` | 60/min per IP | `/oauth/token`, `/oauth/revoke` |
| `throttle:120,1` | 120/min per IP | `/oauth/introspect` |

---

## 8. الـ Database Schema — الـ 8 جداول

### 8.1 OAuth tables

```sql
-- Registered clients (claude.ai + future)
oauth_clients
    id, client_id (unique), client_secret_hash,
    client_type (confidential|public|cimd),
    name, logo_url, homepage_url, description,
    redirect_uris (JSON), allowed_scopes (JSON),
    allowed_grant_types (JSON), allowed_resources (JSON),
    cimd_metadata_url, cimd_metadata_cached_at, cimd_metadata_json,
    require_consent_each_time,
    access_token_ttl_sec (default 900),
    refresh_token_ttl_sec (default 2592000),
    absolute_refresh_ttl_sec (default 7776000),
    created_by_admin_id (nullable — auto-registered clients have no admin),
    revoked_at, notes

-- Active scopes (seeded via OauthScopesSeeder)
oauth_scopes
    id, name (unique), category (read|write|admin),
    description_en, description_ar,
    is_default, is_active

-- Per-admin × client × scope-set consents
oauth_consents
    id, admin_id, client_id, scopes_fingerprint (sha256 unique),
    scopes (JSON), granted_at, revoked_at

-- Authorization codes (60s TTL, single-use)
oauth_authorization_codes
    id, code_hash (sha256 unique),
    admin_id, client_id, redirect_uri, scopes (JSON), resource,
    code_challenge, code_challenge_method (S256),
    expires_at, used_at,
    created_at, created_ip

-- Access tokens (15-min TTL)
oauth_access_tokens
    id, token_hash (sha256 unique),
    admin_id, client_id, scopes (JSON), resource,
    family_id (UUID — links to refresh chain),
    expires_at, revoked_at,
    last_used_at, last_used_ip, use_count,
    created_at, created_ip

-- Refresh tokens (30-day sliding, 90-day absolute)
oauth_refresh_tokens
    id, token_hash, access_token_id, family_id, prev_token_id,
    admin_id, client_id, scopes (JSON), resource,
    expires_at (sliding), absolute_expires_at (hard cap),
    used_at (NULL = active, NOT NULL = rotated or stolen),
    revoked_at

-- Append-only audit log
oauth_audit_log
    id, event_type, outcome (success|failure|blocked),
    failure_reason, client_id, admin_id, token_id, token_kind, family_id,
    ip, user_agent, request_id, metadata (JSON),
    created_at

-- Anomaly signals (impossible-travel, UA drift)
oauth_anomaly_signals
    id, signal_type, severity (low|medium|high),
    status (open|acknowledged|resolved|false_positive),
    admin_id, family_id, client_id,
    consolidation_key (signal_type + admin + family + client),
    first_seen_at, last_seen_at, occurrence_count,
    evidence (JSON),
    resolved_by_admin_id, resolved_at, resolution_notes
```

### 8.2 MCP audit table

```sql
ai_activity_logs (مش OAuth — للـ MCP tools)
    id, ai_session_id (NOT NULL — minted per-admin per-day for REST API),
    admin_id, actor_user_name,
    connector (oauth|legacy), source (api|mcp),
    action_type (investor.update, etc.),
    action_category (read|write|create),
    subject_type, subject_id,
    old_values (JSON), new_values (JSON),
    request_payload (JSON — PII scrubbed),
    response_meta,
    request_id, idempotency_key,
    ip, user_agent,
    created_at

ai_sessions
    id, claude_session_id (UUID), admin_id,
    connector, client_id,
    started_at, ended_at,
    tool_call_count, records_read, records_written
```

---

## 9. الـ Configuration

### 9.1 الـ `.env`

```bash
# OAuth + MCP
APP_URL=https://dashboard.numuangels.net      # CRITICAL — used in all OAuth URLs
APP_ENV=production

# Feature flags
AI_ENABLED=true                                # gates the entire /api/v1/ai/* surface

# Provider keys (Phase 1 enrichment — optional until enrichment used)
PROXYCURL_API_KEY=
HUNTER_API_KEY=

# Enrichment policy
NUMU_AI_ENRICHMENT_CONFIDENCE_MIN=0.80
NUMU_AI_ENRICHMENT_BUDGET_USD_MONTH=150
```

### 9.2 الـ `config/ai.php`

- `enabled` — master switch
- `tiers.read.scopes` / `tiers.full.scopes` — اللي بتسجلها claude.ai
- `enrichment.providers.*` — trust weights لكل provider
- `enrichment.confidence_policy.threshold` — minimum aggregated confidence للـ write

---

## 10. خريطة الفايلات

### 10.1 OAuth Layer

```
app/Models/
    OauthClient.php
    OauthScope.php
    OauthConsent.php
    OauthAuthorizationCode.php
    OauthAccessToken.php
    OauthRefreshToken.php
    OauthAuditLog.php
    OauthAnomalySignal.php

app/Services/Oauth/
    ClientResolver.php             ← CIMD fetch + auto-registration
    PkceVerifier.php               ← S256 verifier, constant-time
    AuthorizeRequestValidator.php  ← 4-phase request validation
    TokenIssuer.php                ← atomic code → token + refresh rotation
    AnomalyDetector.php            ← hot-path impossible-travel + UA drift
    OauthAuditLogger.php           ← all 11 event types

app/Http/Controllers/Api/V1/Oauth/
    OauthWellKnownController.php   ← /.well-known/* JSON
    AuthorizeController.php        ← /oauth/authorize show + decide
    TokenController.php            ← /oauth/token (code + refresh)
    RevokeController.php           ← /oauth/revoke (RFC 7009)
    IntrospectController.php       ← /oauth/introspect (RFC 7662)
    UserinfoController.php         ← /oauth/userinfo

app/Http/Middleware/
    AdminJwtAuthSoft.php           ← optional admin auth on /oauth/authorize
    OAuthBearerAuth.php            ← bearer auth for AS endpoints

app/Console/Commands/Oauth/
    PruneOauthExpiredCommand.php   ← every 5min cron — drops expired rows

database/migrations/
    2026_06_08_200000_create_oauth_clients_table.php
    2026_06_08_200001_create_oauth_scopes_table.php
    2026_06_08_200002_create_oauth_consents_table.php
    2026_06_08_200003_create_oauth_authorization_codes_table.php
    2026_06_08_200004_create_oauth_access_tokens_table.php
    2026_06_08_200005_create_oauth_refresh_tokens_table.php
    2026_06_08_200006_create_oauth_audit_log_table.php
    2026_06_08_200007_create_oauth_anomaly_signals_table.php
    2026_06_08_400000_make_oauth_client_creator_nullable.php

database/seeders/
    OauthScopesSeeder.php          ← seeds the 4 scopes — MUST run on prod
    OauthClaudeClientSeeder.php    ← OPTIONAL (auto-register handles it)
```

### 10.2 MCP Layer

```
app/Http/Controllers/Api/V1/Ai/
    AiMcpController.php            ← Streamable HTTP dispatch + per-tool scope check
    AiInvestorController.php       ← read tools (list/search/get/find_gaps)
    AiInvestorWriteController.php  ← investor_update + bulk_update
    AiStartupController.php
    AiStartupWriteController.php
    AiTaskController.php
    AiTagController.php / AiTagWriteController.php
    AiAssetUploadController.php
    ... etc

app/Http/Middleware/
    McpAuth.php                    ← Bearer validation + audience + scope hierarchy

app/Services/Ai/Mcp/
    McpToolRegistry.php            ← 34 tools, scope overrides, schema
```

### 10.3 REST `/api/v1/investors`

```
app/Http/Controllers/Api/V1/
    InvestorApiController.php      ← GET — eager loads + caching + audit
    InvestorApiWriteController.php ← PATCH — 38 writable fields + tag sync

app/Services/Investors/
    InvestorQueryBuilder.php       ← all filter logic (FILTER_ALIASES,
                                     NAME_FILTER_TO_LABEL_KEY)

app/Http/Resources/
    InvestorResource.php           ← full response shape with display_names

database/migrations/
    2026_06_08_300000_add_indexes_for_investors_api.php
```

### 10.3-b REST `/api/v1/startups` (parity with investors, added 2026-06-09)

```
app/Http/Controllers/Api/V1/
    StartupApiController.php       ← GET — eager loads + caching + audit
    StartupApiWriteController.php  ← PATCH — name resolution + 36 writable
                                     fields + RESOLVE_MAP for {group, sector,
                                     action, status_label, ... 11 keys}

app/Services/Startups/
    StartupQueryBuilder.php        ← all filter logic — mirrors investor
                                     pattern + NAME_FILTER_TO_LABEL_KEY for
                                     11 startup label dimensions

app/Http/Resources/
    StartupResource.php             ← full response shape with MANDATORY
                                      display_names block (16 keys including
                                      group, sector, action, status_label,
                                      stage, traction, country, city)
```

### 10.4 Bridge + Tools (legacy)

```
tools/mcp-stdio-bridge.php         ← stdio↔HTTP bridge for older clients
tools/README.md                    ← setup guide
```

### 10.5 Admin UI

```
app/Http/Controllers/Admin/Ai/
    AiSessionAdminController.php       ← /admin/ai/sessions
    AiActivityAdminController.php      ← /admin/ai/activity  (with rollback)
    AiTaskAdminController.php          ← /admin/ai/tasks
    AiRollbackAdminController.php      ← /admin/ai/rollback (range)
    AiOauthSessionsAdminController.php ← /admin/ai/connectors/oauth (Connected Apps)

resources/views/admin/ai/
    _layout.blade.php                  ← sub-nav (6 entries)
    sessions/, activity/, tasks/, rollback/, oauth-sessions/

routes/web.php  (lines ~478-555)       ← admin AI routes inside ai.enabled gate
```

---

## 11. الـ Audit + Rollback

### 11.1 كل write بيكتب row

```php
DB::transaction(function () use ($investor, $changes, ...) {
    // 1. Capture old values
    $oldValues = ...;

    // 2. Apply the change
    $investor->fill($changes)->save();

    // 3. Write audit row (SAME transaction)
    $this->logger->logWrite([
        'admin_id'        => $adminId,
        'connector'       => 'oauth',  // or 'legacy'
        'action_type'     => 'investor.update',
        'action_category' => 'write',
        'subject_type'    => Investor::class,
        'subject_id'      => $investor->id,
        'old_values'      => $oldValues,
        'new_values'      => $changes,
        'request_payload' => [...],   // PII-scrubbed
        'ip'              => $ip,
        'user_agent'      => $ua,
        'request_id'      => $rid,
    ]);
});
```

### 11.2 Rollback surfaces

```
1. Per-row:    POST /admin/ai/activity/{log}/rollback
2. Session:    POST /admin/ai/sessions/{session}/rollback
3. Task:       POST /admin/ai/tasks/{task}/rollback
4. Range:      POST /admin/ai/rollback/range (date range filter)
```

كل واحدة بتنده `AiRollbackService::applyReverse()` اللي:
- بيشوف لو الـ row reversible (في `REVERSIBLE_ACTIONS`)
- بيـ stale-check (لو القيمة الحالية مش matching new_values → skip + log reason)
- بيـ apply reverse في DB::transaction
- بيكتب audit row جديد بـ action_type=`*.rollback`

### 11.3 الـ REVERSIBLE_ACTIONS

```php
const REVERSIBLE_ACTIONS = [
    'investor.update',
    'investor.bulk_update',
    'investor.upload_avatar',
    'startup.update',
    'startup.bulk_update',
    'startup.upload_logo',
    'tag.attach',
    'tag.detach',
];
```

الأحداث `*.create` و `*.delete` غير قابلة للـ rollback (تحتاج logic إضافي).

---

## 12. Cron Jobs (في `routes/console.php`)

```php
// OAuth maintenance — every 5min
Schedule::command('oauth:prune-expired')
    ->everyFiveMinutes()
    ->withoutOverlapping(60)
    ->runInBackground();
// → يحذف authorization_codes منتهية >5min
// → access_tokens منتهية >7d
// → refresh_tokens absolute_expires_at >30d مضت
```

---

## 13. كيفية التعديل — استرشادات عملية

### 13.1 إضافة tool MCP جديدة

```php
// 1. في app/Services/Ai/Mcp/McpToolRegistry.php
//    ضيف entry جديد في الـ array:
[
    'name'        => 'investor_archive',
    'tier'        => 'full',                      // أو 'read'
    'title'       => 'Archive an investor',
    'description' => '...',                        // Claude بتقراها
    'schema'      => [
        'type'       => 'object',
        'required'   => ['id'],
        'properties' => [
            'id' => ['type' => 'integer', 'minimum' => 1],
        ],
    ],
    'handler' => function (array $a, Request $r) {
        $investor = Investor::findOrFail((int) $a['id']);
        $investor->delete();  // soft delete
        return ['archived' => true, 'id' => $investor->id];
    },
],

// 2. لو محتاج scope مختلف عن tier-default:
//    في const SCOPE_OVERRIDES ضيف:
'investor_archive' => 'numu:admin',
```

بعد الـ deploy، Claude بتشوف الأداة الجديدة لما تعمل `tools/list`.

### 13.2 إضافة filter جديد على `/api/v1/investors`

```php
// 1. في InvestorQueryBuilder::build() ضيف method call:
$this->applyMyNewFilter($query, $request);

// 2. اعمل method جديد:
private function applyMyNewFilter(Builder $query, Request $request): void
{
    $value = $request->query('my_new_filter');
    if ($value === null) return;
    $query->where('investors.some_column', $value);
}

// 3. في InvestorApiController::describeAppliedFilters()
//    ضيف 'my_new_filter' للـ $keys array.
```

### 13.3 إضافة writable field

```php
// 1. في InvestorApiWriteController::ALLOWED_FIELDS ضيف الـ name
// 2. في InvestorApiWriteController::fieldRules() ضيف validation
// 3. (لو محتاج عبر MCP) في AiInvestorWriteController::ALLOWED_FIELDS و fieldRules نفس الشيء
// 4. (لو محتاج يظهر في الـ read) في InvestorResource::toArray() ضيفه
```

### 13.4 إضافة OAuth scope جديد

```php
// 1. في OauthScopesSeeder ضيف:
['name' => 'numu:audit', 'category' => 'read', 'is_default' => false],

// 2. shop شغّل: php artisan db:seed --class=OauthScopesSeeder --force

// 3. في McpAuth::expandScopeHierarchy() ضيف لو محتاج هيراركية:
if (in_array('numu:admin', $expanded, true)) {
    $expanded[] = 'numu:audit';
}

// 4. في الـ tools اللي تطلب الـ scope الجديد:
'investor_history_unredacted' => 'numu:audit',  // في SCOPE_OVERRIDES
```

### 13.5 إضافة host موثوق لـ CIMD auto-register

```php
// في app/Services/Oauth/ClientResolver.php
private const CIMD_AUTO_REGISTER_HOSTS = [
    'claude.ai',
    'anthropic.com',
    'my-new-trusted-host.com',  // ← هنا
];
```

---

## 14. Troubleshooting Playbook

### 14.1 "Authorization failed: Unknown or revoked client_id"

**السبب:** الـ client_id اللي Claude بتبعته مش في DB، ومش from a CIMD trusted host.

**الفحص:**
```bash
php artisan tinker --execute='
echo \DB::table("oauth_clients")->where("client_id","like","%claude%")->count();
'
```

**الحل:**
- لو 0 → CIMD auto-register مش شغّال (تأكد إن الـ ClientResolver code deployed)
- لو الـ host مش في trusted list → ضيفه في `ClientResolver::CIMD_AUTO_REGISTER_HOSTS`

### 14.2 الـ /oauth/authorize بيوديني على /admin/dashboard بدل الـ consent

**السبب:** `AdminJwtAuthSoft` مش متطبق على الـ route، فالـ admin authenticated بيتعامل كـ guest.

**الفحص:**
```bash
grep -c "admin.auth.soft" routes/web.php
```

**الحل:** تأكد إن المـ middleware موجود + `php artisan optimize:clear`.

### 14.3 "scopes_supported": []

**السبب:** OauthScopesSeeder ما اتشغّلش على production.

**الحل:**
```bash
php artisan db:seed --class=OauthScopesSeeder --force
```

### 14.4 "Tool requires scope 'numu:read', not granted"

**السبب:** scope hierarchy expansion مش شغّال في `McpAuth::expandScopeHierarchy()`.

**الحل:** تأكد إن الكود متطبق + `php artisan optimize:clear`.

### 14.5 "Bearer," (بدل "Bearer ") في WWW-Authenticate

**السبب:** Implode بـ comma بيـ join "Bearer" مع params بـ ", ".

**الحل:** المفروض الكود فيه:
```php
->header('WWW-Authenticate', 'Bearer ' . implode(', ', $headerParts))
```
(مفيش "Bearer" داخل الـ array).

### 14.6 الـ continue param بيضيع بعد الـ login

**الفحص:**
1. `view source` على شاشة الـ login — لازم تشوف `<input type="hidden" name="continue" value="...">`
2. `AuthController::login()` بيقرا `$request->input('continue')` ويـ validate `isSafeContinueUrl()`

### 14.7 الـ CIMD fetch بيـ timeout

**الفحص:** الـ `oauth_audit_log` فيه `client.cimd_validation_failed` events؟

**الأسباب الشائعة:**
- Firewall بيمنع outbound HTTPS من السيرفر لـ claude.ai → افتح
- The metadata URL غيّر شكلها → تأكد إن الـ URL هو `https://claude.ai/oauth/mcp-oauth-client-metadata` المعروف

### 14.8 Audit log بيقول `ai_session_id cannot be null`

**السبب:** الـ logic بتاع `restapi_<admin>_<date>` session minting لسه ما اتطبقش في الـ write path الجديد.

**الحل:** انسخ النمط من `InvestorApiController::logAuditRow()`.

---

## 15. Deployment Checklist

### 15.1 أول deploy

```bash
# 1. Pull الكود
git pull origin main

# 2. Migrations (10+ جدول)
php artisan migrate --force

# 3. Seeders (critical للـ OAuth)
php artisan db:seed --class=OauthScopesSeeder --force

# 4. Clear caches
php artisan optimize:clear

# 5. Schedule worker (لو مش شغّال)
php artisan schedule:work &

# 6. Verify
curl -I https://dashboard.numuangels.net/api/v1/ai/mcp/full
#    لازم تشوف: WWW-Authenticate: Bearer realm="Numu", ...

curl https://dashboard.numuangels.net/.well-known/oauth-authorization-server
#    لازم تشوف: scopes_supported: ["numu:admin","numu:read",...]
```

### 15.2 Deploy عادي بعد modifications

```bash
git pull origin main
php artisan migrate --force        # لو في migrations جديدة
php artisan optimize:clear         # دائمًا
```

### 15.3 الـ Health checks بعد كل deploy

```bash
# 1. الـ discovery يرد صح
curl -s https://dashboard.numuangels.net/.well-known/oauth-authorization-server | jq '.scopes_supported'
# الناتج المتوقع: ["numu:admin", "numu:read", "numu:enrich", "numu:write"]

# 2. الـ MCP endpoint بيرد بـ 401 + WWW-Authenticate صحيح
curl -I https://dashboard.numuangels.net/api/v1/ai/mcp/full | grep -i www-auth
# الناتج: WWW-Authenticate: Bearer realm="Numu", ...

# 3. الـ scheduler shغّال
php artisan schedule:list

# 4. الـ oauth_audit_log آخر events سليمة
php artisan tinker --execute='
foreach (\DB::table("oauth_audit_log")->latest("id")->limit(5)->get() as $r) {
    echo "$r->id | $r->event_type | $r->outcome | $r->created_at\n";
}'
```

---

## 16. Glossary

| Term | المعنى |
|---|---|
| **MCP** | Model Context Protocol — البروتوكول اللي Claude بتستخدمه لتتكلم مع الـ tools |
| **CIMD** | Client ID Metadata Document — RFC draft حيث الـ client_id هو URL لـ JSON metadata |
| **PKCE** | Proof Key for Code Exchange — حماية من code interception (S256 mandatory في OAuth 2.1) |
| **Family ID** | UUID بيـ link الـ access + refresh tokens — كل rotation بيخلق pair جديد بنفس family |
| **Reuse detection** | لما refresh token مستخدم مرتين → revoke كل الـ family (الـ chain فاضي) |
| **Scope hierarchy** | `admin ⊃ enrich ⊃ write ⊃ read` — granting أعلى scope بيمنح اللي تحته تلقائيًا |
| **Tier** | المستوى اللي الـ tool متاح فيه: read (18 tool) أو full (16 tool إضافية) |
| **Audience** | RFC 8707 resource parameter — الـ token bound to specific resource URL |
| **Continue URL** | الـ workaround للـ login bounce — explicit URL parameter بدل session.url.intended |
| **Streamable HTTP** | الـ MCP transport الجديد (SSE + JSON-RPC) — بديل الـ stdio |

---

## 17. الـ Roadmap — اللي مش متعمل لسه

### 17.1 ممكن في المستقبل
- **Live deploy على claude.ai بدون stdio bridge** — Done ✓ (الحالة الراهنة)
- **Org-wide OAuth admin view** — Super admin يشوف كل sessions لكل الأدمنز
- **PHPUnit feature tests** للـ OAuth flow + MCP tools
- **Anomaly detection warm path** — batch cron job يـ mine الـ audit log
- **Enrichment engine providers جدد** — PDL, Crunchbase, Clearbit
- **Asset upload via investor_upload_avatar tool** — لسه placeholder

### 17.2 Architectural decisions اللي اتأخرت
- **Enrichment preview UI** — اتشال في الـ 2026-06-08 cleanup (الـ MCP tools لسه شغّالة)
- **AI change requests** — اتشال نهائيًا (الـ feature deprecated بعد immediate-apply)
- **Dynamic Client Registration (RFC 7591)** — مش متطبق (CIMD أحدث + preferred)
- **JWKS endpoint** — `jwks_uri: null` — opaque tokens فقط، مفيش JWT signing

---

## 18. Contacts + Support

- **Code Owner:** mohamed.saad.alm222@gmail.com
- **Repo:** Azure DevOps — `dev.azure.com/Lun-Dev/numu/_git/numu`
- **Production:** https://dashboard.numuangels.net
- **OAuth Audit Log:** `oauth_audit_log` table (forensic retention)
- **MCP Audit Log:** `ai_activity_logs` table (PII-scrubbed)
- **Rollback Surface:** https://dashboard.numuangels.net/admin/ai/activity

---

## 19. Quick Reference — Common Commands

```bash
# لـ Check إن claude.ai client متسجّل
php artisan tinker --execute='
foreach (\DB::table("oauth_clients")->where("client_id","like","%claude.ai%")->get() as $c) {
    echo "$c->id | $c->client_id | $c->client_type | active=" . ($c->revoked_at === null ? "yes" : "REVOKED") . "\n";
}'

# عرض active OAuth sessions لكل admin
php artisan tinker --execute='
foreach (\DB::table("oauth_refresh_tokens")->whereNull("revoked_at")->where("absolute_expires_at",">",now())->get() as $r) {
    echo "family=$r->family_id admin=$r->admin_id client=" . substr($r->client_id,0,40) . "\n";
}'

# آخر 10 audit events
php artisan tinker --execute='
foreach (\DB::table("oauth_audit_log")->latest("id")->limit(10)->get() as $r) {
    echo "$r->id | $r->event_type | $r->outcome | $r->created_at\n";
}'

# Revoke كل الـ tokens لـ admin معيّن (لو شك في compromise)
php artisan tinker --execute='
\DB::table("oauth_refresh_tokens")->where("admin_id",614)->update(["revoked_at" => now()]);
\DB::table("oauth_access_tokens")->where("admin_id",614)->update(["revoked_at" => now()]);
echo "All tokens revoked for admin 614.\n";
'

# Force re-fetch of CIMD metadata لـ claude.ai
php artisan tinker --execute='
\DB::table("oauth_clients")->where("client_id","like","%claude.ai%")
    ->update(["cimd_metadata_cached_at" => null]);
echo "Next /oauth/authorize call will re-fetch claude.ai metadata.\n";
'

# Smoke test الـ /api/v1/investors endpoint
TOKEN="naat_..."  # من اللوحة admin/ai/connectors/oauth
curl -s "https://dashboard.numuangels.net/api/v1/investors?group=Angel%20Investors&per_page=5" \
    -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, name: .basic.name, group: .display_names.group}'

# Smoke test الـ /api/v1/startups endpoint
curl -s "https://dashboard.numuangels.net/api/v1/startups?sector=Fintech&per_page=5" \
    -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {id, name: .basic.name, sector: .display_names.sector, group: .display_names.group, stage: .display_names.investment_stage}'

# Smoke test name resolution على startup_update
curl -s -X PATCH "https://dashboard.numuangels.net/api/v1/startups/42" \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d '{"group":"Pipeline","sector":"Fintech","action":"Approve","rationale":"manual review"}' \
  | jq '{updated_fields: .meta.updated_fields, name_resolved: .meta.name_resolved, unresolved: .meta.unresolved}'

# Fetch startup filter vocabulary
curl -s -X POST "https://dashboard.numuangels.net/api/v1/ai/mcp/read" \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"startup_filter_dictionary","arguments":{"kind":"all"}}}' \
  | jq '.result.structuredContent | {groups: (.groups | length), label_kinds: (.label_options | keys), total: .meta.total_label_options}'
```

---

**End of documentation.**
