Machine-first. Dense. No prose filler. Read this when MCP is unavailable. Source: https://github.com/tata1mg/catalyst-core | Version: 0.1.0-canary.4
React SSR framework + universal native app builder (Android/iOS via WebView). Not a bare React app. Not Next.js. Opinionated SSR lifecycle, static route array, server/client fetcher pattern.
Key exports from catalyst-core package:
catalyst-core— SSR engine,<Head>,<Body>,<Outlet>@tata1mg/router— router package (react-router-v6 wrapper),useNavigate,Link,useCurrentRouteData,RouterDataProvidercatalyst-core/hooks— native bridge hooks:useCamera,useFilePicker,useHapticFeedback,useDeviceInfo,useIntentcatalyst-core/WebBridge— low-level WebBridge (JS↔native), prefer hooks over thiscatalyst-core/caching— server-side caching utilities
GitHub: https://github.com/tata1mg/catalyst-core (MIT, public)
Hooks source: src/native/bridge/hooks.js
Entry point: dist/index.js
<project-root>/
├── config/
│ └── config.json ← required. env vars + WEBVIEW_CONFIG
├── src/
│ └── js/
│ ├── routes/
│ │ └── index.js ← required. exports default route array
│ ├── containers/
│ │ └── App/
│ │ └── index.js ← required. App shell with <Outlet />
│ └── store/
│ └── index.js ← optional. configureStore(initialState, request)
├── server/
│ ├── index.js ← required. lifecycle hooks (preServerInit etc)
│ ├── server.js ← required. addMiddlewares(app)
│ └── document.js ← required. Document component
├── client/
│ ├── index.js ← required. client hydration entry
│ └── styles.js ← required. global CSS imports
├── public/
│ ├── offline.html ← required for native. shown when device offline
│ ├── android/
│ │ ├── splashscreen.png ← required for native Android
│ │ └── appIcons/ ← mdpi hdpi xhdpi xxhdpi xxxhdpi .png
│ └── ios/
│ ├── splashscreen.png ← required for native iOS
│ └── appIcons/ ← 20 29 40 58 60 80 87 120 1024 .png
└── package.json ← anchor. must have catalyst-core in deps
MCP anchor rule: Every MCP tool hard-fails if package.json has no catalyst-core dependency.
{
"NODE_SERVER_PORT": 3005,
"API_URL": "https://api.example.com",
"CLIENT_ENV_KEYS": ["API_URL"],
"splashScreen": {
"duration": 1000,
"backgroundColor": "#ffffff"
},
"WEBVIEW_CONFIG": {
"useHttps": false,
"android": {
"buildType": "debug",
"sdkPath": "/path/to/android/sdk",
"emulatorName": "Pixel_6_API_33",
"appName": "MyApp",
"appBundleId": "com.example.myapp",
"buildOptimisation": false,
"cachePattern": "*.css,*.js"
},
"ios": {
"buildType": "Debug",
"simulatorName": "iPhone 15",
"appName": "MyApp",
"appBundleId": "com.example.myapp"
},
"accessControl": {
"enabled": true,
"allowedUrls": ["https://api.example.com/*", "*.example.com"]
},
"notifications": {
"enabled": false
}
}
}Field rules:
splashScreenis top-level, NOT insideWEBVIEW_CONFIGandroid.buildType: lowercase"debug"|"release"— debug disables caching (required for HMR)ios.buildType: PascalCase"Debug"|"Release"— case-sensitiveappBundleId: reverse-DNS formatcom.org.appnameCLIENT_ENV_KEYS: only listed keys are sent to browser bundle — security boundaryaccessControl.enabled: false→ all URLs allowed;true→ only allowedUrls pass
preServerInit()→ server startsApp.serverSideFunction({ store, req, res })→ runs on every requestPage.serverFetcher({ route, location, params, searchParams, navigate }, { store })→ runs for matched route- React SSR renders HTML → sent to client
- Client hydrates →
Page.clientFetcherruns on next client navigation
| serverFetcher | clientFetcher | |
|---|---|---|
| Runs on | Server only | Client only (on navigation) |
| When | First request / window.location.href nav |
<Link>, useNavigate(), <Navigate> |
| Bundle | Excluded from client bundle | Included |
| navigate() | Server redirect (response.send) | Client-side redirect |
| Safe for | Server secrets, DB calls | User interactions |
// Page component
const Page = () => <div>{data}</div>
Page.serverFetcher = async ({ route, params, navigate }, { store }) => {
return await fetch("/api/data").then((r) => r.json())
}
Page.clientFetcher = async ({ route, params, navigate }, { store }, customArg) => {
return await fetch("/api/data").then((r) => r.json())
}// Consuming fetcher data
import { useCurrentRouteData } from "@tata1mg/router"
const Page = () => {
const { data, isFetching, error, refetch, clear } = useCurrentRouteData()
}export const preServerInit = () => {} // before server starts
export const onServerError = (err) => {} // server failed to start
export const onRouteMatch = (ctx) => {} // after route matching (hit or miss)
export const onFetcherSuccess = (ctx) => {} // after serverFetcher runs
export const onRenderError = (err, ctx) => {} // render failed
export const onRequestError = (err, ctx) => {} // outermost catch// src/js/routes/index.js
import HomePage from "@containers/HomePage"
import AboutPage from "@containers/AboutPage"
const routes = [
{ path: "/", end: true, component: HomePage },
{ path: "/about", end: false, component: AboutPage },
]
export default routes// src/js/routes/utils.js — must export RouterDataProvider
import { RouterDataProvider } from "@tata1mg/router"
export { RouterDataProvider }// App shell — src/js/containers/App/index.js
import React from "react"
import { Outlet } from "@tata1mg/router"
const App = () => (
<>
<Outlet />
</>
)
App.serverSideFunction = ({ store, req, res }) => new Promise((resolve) => resolve())
export default AppNavigation:
useNavigate()from@tata1mg/router— client nav<Link to="/path">— client navnavigate()insideserverFetcher— server redirectnavigate()insideclientFetcher— client redirect
All hooks live in catalyst-core/hooks. All hooks expose isNative (bool) and isWeb (bool).
Hooks throw if window.WebBridge is not initialized (native-only context).
import { useCamera } from "catalyst-core/hooks"
const { data, loading, isNative, isWeb, execute, takePhoto, permission } = useCamera()
// data: { fileSrc, fileName, size, mimeType, transport }
// execute() / takePhoto() — triggers camera
// permission: permission state objectimport { useFilePicker } from "catalyst-core/hooks"
const { data, isNative, isWeb, execute, pickFile, getFileObject, getAsBase64 } = useFilePicker()
// execute(mimeType?) / pickFile(mimeType?)
// data: { fileSrc, fileName, size, mimeType, transport }
// getFileObject(idx) — returns File-like object
// getAsBase64(idx) — returns base64 stringimport { useHapticFeedback } from "catalyst-core/hooks"
const { execute, trigger, isSupported, isNative, isWeb } = useHapticFeedback()
// execute(type) / trigger(type)
// type: 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error'
// isSupported: bool — false on web or unsupported devicesimport { useDeviceInfo } from "catalyst-core/hooks"
const { data, isNative, isWeb } = useDeviceInfo()
// data: { model, manufacturer, platform, screenWidth, screenHeight, screenDensity }
// platform: 'android' | 'ios'import { useIntent } from "catalyst-core/hooks"
const { execute } = useIntent()
// execute(url, mimeType) — opens file/URL in native app// CORRECT — works inside native WebView
const isNative = window.__PLATFORM__ === "android" || window.__PLATFORM__ === "ios"
const platform = window.__PLATFORM__ // 'android' | 'ios' | undefined
// WRONG — unreliable inside WebView
navigator.userAgent / // returns WebView UA string, NOT 'Android'/'iPhone'
Android /
i.test(navigator.userAgent) // may return false inside native WebViewconst { getDeviceInfo } = WebBridge.init() // init and destructure
const { getDeviceInfo } = window.WebBridge // direct access
const info = await getDeviceInfo()
// Returns: { model, manufacturer, platform, screenWidth, screenHeight, screenDensity }20 conversion tasks across 3 tiers. Use MCP get_conversion_status to auto-detect state.
| ID | Task | Key check |
|---|---|---|
| T1_CONFIG | config/config.json with NODE_SERVER_PORT, WEBVIEW_CONFIG, API_URL | file exists + fields present |
| T2_ROUTER_DEP | @tata1mg/router in package.json deps | not react-router-dom |
| T3_ROUTES_FILE | src/js/routes/index.js exports route array | exports default [...] |
| T4_DATA_FETCHING | Page data via serverFetcher/clientFetcher, not useEffect+fetch | no page-level useEffect fetch |
| T5_ROUTER_DATA_PROVIDER | RouterDataProvider wired in routes/utils.js | file + RouterDataProvider present |
| T6_APP_SHELL | App/index.js renders <Outlet /> |
Outlet present |
| T7_SERVER_FILES | server/index.js, server/server.js, server/document.js | all 3 exist |
| T8_CLIENT_ENTRY | client/index.js, client/styles.js | both exist |
| ID | Task | Key check |
|---|---|---|
| T9_WEBVIEW_ANDROID | WEBVIEW_CONFIG.android with buildType, sdkPath, emulatorName, appName | all 4 fields |
| T10_WEBVIEW_IOS | WEBVIEW_CONFIG.ios with buildType, appBundleId, simulatorName, appName | all 4 fields |
| T11_ACCESS_CONTROL | WEBVIEW_CONFIG.accessControl.enabled=true + allowedUrls non-empty | both set |
| T12_SPLASH_SCREEN | splashScreen at top level + public/android/splashscreen.png + public/ios/splashscreen.png | config + files |
| T13_ANDROID_ICONS | public/android/appIcons/{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}.png | all 5 |
| T14_IOS_ICONS | public/ios/appIcons/{20,29,40,58,60,80,87,120,1024}.png | all 9 |
| T15_OFFLINE_HTML | public/offline.html | file exists |
| ID | Task | Trigger | Correct pattern |
|---|---|---|---|
| T17a_USE_FILEPICKER | <input type="file"> found |
useFilePicker from catalyst-core/hooks | |
| T17b_USE_CAMERA | <input capture> found |
useCamera from catalyst-core/hooks | |
| T18_USE_HAPTIC | navigator.vibrate() found | useHapticFeedback from catalyst-core/hooks | |
| T19_USE_NOTIFICATIONS | push notification code found | notifications.enabled=true + Firebase files | |
| T20_USE_DEVICE_INFO | navigator.userAgent sniffing found | window.PLATFORM |
depends_on graph:
T1 → T2 → T3 → T4
T3 → T5 → T6 → T8 → T17a, T17b, T18, T20
T1 → T7
T1 → T9 → T13
T1 → T10 → T14
T1 → T11, T12, T15
T9 + T10 → T19
npm run start # dev server — port 3005
npm run build # production build
npm run serve # serve production build
npm run setupEmulator:android # configure Android emulator
npm run setupEmulator:ios # configure iOS simulator
npm run buildApp:android # build + install on Android emulator
npm run buildApp:ios # build + install on iOS simulatornpm run start(keep running)npm run buildApp:android- Reads
WEBVIEW_CONFIG.androidfrom config.json - Requires:
sdkPath,emulatorName,appName,buildType - For release: requires keystore +
appBundleId→ produces.aab - For debug: HMR works, no caching (
buildType: "debug")
- Reads
npm run start(keep running)npm run buildApp:ios- Reads
WEBVIEW_CONFIG.iosfrom config.json - Requires:
buildType(PascalCase),simulatorName,appName,appBundleId - Requires Xcode +
xcode-select --install
- Reads
buildOptimisation: true→ static assets bundled into APK, loaded from device storage- ~90% faster initial load, near-zero network requests for JS/CSS
- Disable during development (
buildOptimisation: false,buildType: "debug")
- Fresh (< 24h): served from cache, no network
- Stale (24–25h): served from cache + background revalidation
- Expired (> 25h): network fetch, cache updated
Use MCP debug_issue with symptom text. Matches via keyword scoring against known_errors table.
sdkPath not found / Android SDK error
- Check
WEBVIEW_CONFIG.android.sdkPathpoints to actual SDK dir (not Studio install dir) - Find it: Android Studio → SDK Manager → SDK Location at top
appBundleId invalid
- Must be reverse-DNS:
com.org.app— no uppercase, no spaces, 3+ segments
buildType case error
- Android: lowercase
"debug"/"release" - iOS: PascalCase
"Debug"/"Release"
Blank page on native / hydration mismatch
<Outlet />missing from App/index.js- RouterDataProvider not in routes/utils.js
- serverFetcher not returning data (check server logs)
Push notifications silent failure
notifications.enablednot set totruein WEBVIEW_CONFIGgoogle-services.json(Android) orGoogleService-Info.plist(iOS) missing from project root
useEffect+fetch data not loading on native
- Page-level data must use
serverFetcher/clientFetcher useEffect+fetchworks on web, fails SSR hydration → blank page on native first load
navigator.vibrate() does nothing on device
- Native WebView ignores Web Vibration API silently
- Replace with
useHapticFeedback().execute('medium')
File picker / camera silently does nothing on Android
<input type="file">/<input capture>unreliable in WebView- Replace with
useFilePicker().execute()/useCamera().execute()
window.WebBridge is not defined
- Hook called outside native context, or before bridge initialised
- Add
isNativeguard:const { isNative, execute } = useCamera(); if (isNative) execute()
MCP server lives at mcp_v2/mcp.js. Reads context.db (SQLite). Requires catalyst-core project root.
| Tool | When to call | Key params |
|---|---|---|
get_conversion_status |
Full project scan — gaps / done / needs_review / blocked | include_not_applicable |
get_conversion_tasks |
Filtered actionable task list | filter: all|critical|native|enhancements |
check_config |
Validate config/config.json against schema | platform: android|ios|both |
debug_issue |
"Why is X failing?" — keyword matches known errors | symptom: string |
get_build_flow |
Step-by-step build instructions | platform, mode, symptom? |
get_architecture_diagram |
ASCII architecture diagram | feature: string (free text) |
create_task_plan |
Start tracked conversion plan from live scan | goal: string |
update_task_step |
Mark step done/blocked/in_progress | step_index, status, note? |
get_active_task |
Resume after context reset — show current step | include_all_steps? |
sync_catalyst_docs |
Sync latest docs from GitHub | (no params) |
MCP design rules:
- No tool re-reads package.json — it's loaded once at startup
create_task_planruns liveget_conversion_statusscan before writing planneeds_reviewtasks are resolved inline (signal_files read from disk) before plan is written- Final plan has no
needs_reviewsteps — onlypending,done,blocked bare_minimumin plan output = Tier 1 + Tier 2 gaps, topologically sorted = first native build checklistget_active_taskis the cold-start resume tool — call this first after any context reset
Task plan step statuses: pending → in_progress → done | blocked | skipped