auramaxx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +77 -0
- package/apps/desktop-electron/main.js +428 -0
- package/bin/auramaxx.js +1063 -0
- package/docs/ADAPTERS.md +466 -0
- package/docs/AGENT_SETUP.md +159 -0
- package/docs/API.md +127 -0
- package/docs/APPS.md +199 -0
- package/docs/ARCHITECTURE.md +235 -0
- package/docs/AUTH.md +318 -0
- package/docs/BEST-PRACTICES.md +82 -0
- package/docs/CLI.md +141 -0
- package/docs/DESKTOP_ELECTRON.md +26 -0
- package/docs/DEVELOPING-APPS.md +453 -0
- package/docs/MCP.md +122 -0
- package/docs/PACKAGING_POLICY.md +19 -0
- package/docs/PERMISSION.md +137 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/README.md +50 -0
- package/docs/SKILLS.md +132 -0
- package/docs/TROUBLESHOOTING.md +376 -0
- package/docs/WORKSPACE.md +673 -0
- package/docs/agent-auth.md +14 -0
- package/docs/api/authentication.md +79 -0
- package/docs/api/secrets/api-keys.md +28 -0
- package/docs/api/secrets/credentials.md +80 -0
- package/docs/api/secrets/sharing.md +48 -0
- package/docs/api/system.md +41 -0
- package/docs/api/wallets/apps-strategies.md +66 -0
- package/docs/api/wallets/core.md +46 -0
- package/docs/api/wallets/data-portfolio.md +42 -0
- package/docs/aura-file.md +48 -0
- package/docs/core-concepts/FEATURES.md +114 -0
- package/docs/credentials.md +120 -0
- package/docs/external/HOW_TO_AURAMAXX/GETTING_SECRETS.md +33 -0
- package/docs/external/HOW_TO_AURAMAXX/README.md +45 -0
- package/docs/external/getting-started.md +10 -0
- package/docs/external/overview.md +19 -0
- package/docs/external/persona-paths.md +7 -0
- package/docs/external/share-secret.md +76 -0
- package/docs/external/why-aura.md +7 -0
- package/docs/security.md +227 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +28 -0
- package/package.json +167 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/20260222090000_update_admin_ttl_default/migration.sql +10 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/actions.ts +474 -0
- package/server/cli/commands/api.ts +220 -0
- package/server/cli/commands/apikey.ts +277 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/auth.ts +464 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/diary.ts +274 -0
- package/server/cli/commands/doctor.ts +1247 -0
- package/server/cli/commands/env.ts +476 -0
- package/server/cli/commands/experimental.ts +69 -0
- package/server/cli/commands/init.ts +798 -0
- package/server/cli/commands/lock.ts +157 -0
- package/server/cli/commands/mcp.ts +285 -0
- package/server/cli/commands/quickhack.ts +86 -0
- package/server/cli/commands/release-check.ts +231 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/service.ts +320 -0
- package/server/cli/commands/shell-hook.ts +512 -0
- package/server/cli/commands/skill.ts +216 -0
- package/server/cli/commands/start.ts +139 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +36 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +50 -0
- package/server/cli/commands/vault.ts +1323 -0
- package/server/cli/commands/wallet.ts +209 -0
- package/server/cli/index.ts +280 -0
- package/server/cli/lib/approval-poll.ts +94 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +280 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/escalation.ts +57 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/lock-unlock-helper.ts +71 -0
- package/server/cli/lib/process.ts +162 -0
- package/server/cli/lib/prompt.ts +294 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +579 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +420 -0
- package/server/lib/adapters/factory.ts +119 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +419 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +258 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/approval-link.ts +27 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/auto-execute.ts +160 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +1048 -0
- package/server/lib/config.ts +408 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +111 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +608 -0
- package/server/lib/credential-scope.ts +102 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +533 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +422 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +58 -0
- package/server/lib/defaults.ts +386 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/diary.ts +34 -0
- package/server/lib/dont-ask-again-policy.ts +41 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +114 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +217 -0
- package/server/lib/feature-flags.ts +93 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/human-action-summary.ts +80 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +340 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +230 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +71 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +299 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +297 -0
- package/server/lib/resolve-action.ts +328 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/secret-gist-share.ts +296 -0
- package/server/lib/sessions.ts +634 -0
- package/server/lib/socket-path.ts +56 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +159 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +237 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +84 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/update-check.ts +35 -0
- package/server/lib/verified-summary.ts +414 -0
- package/server/lib/view-registry.ts +80 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +1589 -0
- package/server/mcp/tools.ts +276 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +539 -0
- package/server/routes/adapters.ts +711 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +343 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +406 -0
- package/server/routes/backup.ts +404 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +380 -0
- package/server/routes/credential-vaults.ts +159 -0
- package/server/routes/credentials.ts +1782 -0
- package/server/routes/dashboard.ts +97 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/flags.ts +11 -0
- package/server/routes/fund.ts +225 -0
- package/server/routes/heartbeat.ts +375 -0
- package/server/routes/import.ts +364 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +366 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +352 -0
- package/server/routes/swap-solana.ts +176 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +467 -0
- package/server/routes/views.ts +41 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +709 -0
- package/server/types.ts +146 -0
- package/shared/credential-field-schema.ts +248 -0
- package/skills/auramaxx/HEARTBEAT.md +78 -0
- package/skills/auramaxx/SKILL.md +745 -0
- package/skills/auramaxx/docs/AGENT_SETUP.md +155 -0
- package/skills/auramaxx/docs/API.md +127 -0
- package/skills/auramaxx/docs/AUTH.md +318 -0
- package/skills/auramaxx/docs/CLI.md +130 -0
- package/skills/auramaxx/docs/MCP.md +122 -0
- package/skills/auramaxx/docs/TROUBLESHOOTING.md +357 -0
- package/skills/auramaxx/docs/WORKSPACE.md +673 -0
- package/skills/auramaxx/docs/security.md +227 -0
- package/skills/task-lifecycle/SKILL.md +378 -0
- package/src/app/api/[...doc]/page.tsx +36 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +132 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/docs/plain/route.ts +74 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +290 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +40 -0
- package/src/app/api/workspace/config/route.ts +121 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app-legacy-do-not-use/page.tsx +2245 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/approve/[actionId]/page.tsx +409 -0
- package/src/app/docs/DocsPageContent.tsx +269 -0
- package/src/app/docs/[...doc]/page.tsx +41 -0
- package/src/app/docs/page.tsx +38 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +819 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +102 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/page.tsx +1964 -0
- package/src/app/privacy/page.tsx +63 -0
- package/src/app/providers.tsx +87 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/app/terms/page.tsx +80 -0
- package/src/components/ChainSelector.tsx +44 -0
- package/src/components/HumanActionBar.tsx +697 -0
- package/src/components/NotificationDrawer.tsx +387 -0
- package/src/components/PasskeyEnrollmentPrompt.tsx +235 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +88 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +147 -0
- package/src/components/design-system/ConfirmationModal.tsx +107 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/DownloadButton.tsx +149 -0
- package/src/components/design-system/Drawer.tsx +133 -0
- package/src/components/design-system/FilterDropdown.tsx +183 -0
- package/src/components/design-system/ItemPicker.tsx +157 -0
- package/src/components/design-system/Modal.tsx +296 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +65 -0
- package/src/components/design-system/TyvekCollapsibleSection.tsx +55 -0
- package/src/components/design-system/index.ts +14 -0
- package/src/components/docs/ClientSideMarkdown.tsx +51 -0
- package/src/components/docs/DocsSearchBar.tsx +118 -0
- package/src/components/docs/DocsThemeToggle.tsx +38 -0
- package/src/components/docs/PersistentDocGroup.tsx +91 -0
- package/src/components/docs/ShareUrlButton.tsx +33 -0
- package/src/components/docs/SidebarScrollMemory.tsx +56 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/CreateViewModal.tsx +88 -0
- package/src/components/layout/LeftRail.tsx +114 -0
- package/src/components/layout/TabBar.tsx +284 -0
- package/src/components/layout/WalletSidebar.tsx +1030 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/marketing/AuraMaxxSpecOverlay.tsx +653 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1272 -0
- package/src/components/vault/AuditConsole.tsx +600 -0
- package/src/components/vault/CredentialDetail.tsx +625 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +583 -0
- package/src/components/vault/CredentialForm.tsx +1484 -0
- package/src/components/vault/CredentialList.tsx +265 -0
- package/src/components/vault/CredentialRow.tsx +130 -0
- package/src/components/vault/CredentialShareModal.tsx +273 -0
- package/src/components/vault/CredentialVault.tsx +1662 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/DocsConsole.tsx +113 -0
- package/src/components/vault/ImportCredentialsModal.tsx +578 -0
- package/src/components/vault/LargeTypeModal.tsx +88 -0
- package/src/components/vault/PasswordGenerator.tsx +232 -0
- package/src/components/vault/TOTPDisplay.tsx +108 -0
- package/src/components/vault/TotpSetupPanel.tsx +198 -0
- package/src/components/vault/VaultSidebar.tsx +881 -0
- package/src/components/vault/credentialFormName.ts +91 -0
- package/src/components/vault/hooks/useVaultKeyboardShortcuts.ts +69 -0
- package/src/components/vault/types.ts +56 -0
- package/src/context/AuthContext.tsx +365 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAgentActions.ts +552 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/hooks/useTheme.ts +156 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api-docs.ts +154 -0
- package/src/lib/api.ts +474 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/credential-field-schema.ts +11 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +544 -0
- package/src/lib/events.ts +363 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/totp-import.ts +57 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/view-registry.ts +57 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +170 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,1589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AuraMaxx MCP Server
|
|
4
|
+
* =====================
|
|
5
|
+
* Standalone MCP stdio server that gives any AI agent access to the wallet API.
|
|
6
|
+
*
|
|
7
|
+
* Auth bootstrap (in order):
|
|
8
|
+
* 1. Unix socket auto-approve (ephemeral RSA keypair, encrypted token, zero config)
|
|
9
|
+
* 2. AURA_TOKEN env var (CI/CD fallback)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx tsx server/mcp/server.ts
|
|
13
|
+
* AURA_TOKEN=<token> npx tsx server/mcp/server.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import * as net from 'net';
|
|
22
|
+
import { spawn } from 'child_process';
|
|
23
|
+
import {
|
|
24
|
+
constants,
|
|
25
|
+
createDecipheriv,
|
|
26
|
+
generateKeyPairSync,
|
|
27
|
+
privateDecrypt,
|
|
28
|
+
} from 'crypto';
|
|
29
|
+
import { TOOLS, executeTool, jsonSchemaToZod } from './tools.js';
|
|
30
|
+
import {
|
|
31
|
+
evaluateProjectScopeAccess,
|
|
32
|
+
emitProjectScopeEvent,
|
|
33
|
+
type ProjectScopeMode,
|
|
34
|
+
} from '../lib/project-scope';
|
|
35
|
+
import { resolveAuraSocketCandidates } from '../lib/socket-path';
|
|
36
|
+
import {
|
|
37
|
+
appendDiaryEntry,
|
|
38
|
+
countDiaryEntries,
|
|
39
|
+
formatDiaryEntry,
|
|
40
|
+
getDiaryCredentialName,
|
|
41
|
+
getLegacyDiaryCredentialName,
|
|
42
|
+
resolveDiaryDate,
|
|
43
|
+
} from '../lib/diary';
|
|
44
|
+
import {
|
|
45
|
+
getCredentialFieldValue,
|
|
46
|
+
NOTE_CONTENT_KEY,
|
|
47
|
+
} from '../../shared/credential-field-schema';
|
|
48
|
+
|
|
49
|
+
let token: string | undefined = process.env.AURA_TOKEN;
|
|
50
|
+
|
|
51
|
+
/** Tracks the most recent auth request so get_token can report status. */
|
|
52
|
+
let pendingAuth: {
|
|
53
|
+
requestId: string;
|
|
54
|
+
agentId: string;
|
|
55
|
+
status: 'polling' | 'approved' | 'rejected' | 'timeout';
|
|
56
|
+
approveUrl: string;
|
|
57
|
+
} | null = null;
|
|
58
|
+
|
|
59
|
+
const WALLET_BASE = () => process.env.WALLET_SERVER_URL || 'http://127.0.0.1:4242';
|
|
60
|
+
|
|
61
|
+
// ── Socket Bootstrap ───────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
interface HybridEnvelope {
|
|
64
|
+
v: number;
|
|
65
|
+
alg: string;
|
|
66
|
+
key: string;
|
|
67
|
+
iv: string;
|
|
68
|
+
tag: string;
|
|
69
|
+
data: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Decrypt an encrypted blob (token or credential) using our private key.
|
|
74
|
+
* Supports both direct RSA-OAEP and hybrid RSA-OAEP/AES-256-GCM envelopes.
|
|
75
|
+
*/
|
|
76
|
+
function decryptWithPrivateKey(encryptedBase64: string, privateKeyPem: string): string {
|
|
77
|
+
const decoded = Buffer.from(encryptedBase64, 'base64');
|
|
78
|
+
let envelope: HybridEnvelope;
|
|
79
|
+
try {
|
|
80
|
+
envelope = JSON.parse(decoded.toString('utf8')) as HybridEnvelope;
|
|
81
|
+
} catch {
|
|
82
|
+
// Direct RSA-OAEP ciphertext (small payloads)
|
|
83
|
+
return privateDecrypt(
|
|
84
|
+
{ key: privateKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
85
|
+
decoded,
|
|
86
|
+
).toString('utf8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (envelope.v !== 1 || envelope.alg !== 'RSA-OAEP/AES-256-GCM') {
|
|
90
|
+
throw new Error(`Unexpected envelope: v=${envelope.v} alg=${envelope.alg}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sessionKey = privateDecrypt(
|
|
94
|
+
{ key: privateKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
95
|
+
Buffer.from(envelope.key, 'base64'),
|
|
96
|
+
);
|
|
97
|
+
const decipher = createDecipheriv('aes-256-gcm', sessionKey, Buffer.from(envelope.iv, 'base64'));
|
|
98
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
|
|
99
|
+
return Buffer.concat([
|
|
100
|
+
decipher.update(Buffer.from(envelope.data, 'base64')),
|
|
101
|
+
decipher.final(),
|
|
102
|
+
]).toString('utf8');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Ephemeral keypair for this session (used for bootstrap + credential reads)
|
|
106
|
+
const { publicKey: ephemeralPubPem, privateKey: ephemeralPrivPem } = generateKeyPairSync('rsa', {
|
|
107
|
+
modulusLength: 2048,
|
|
108
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
109
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** Token TTL from bootstrap (seconds). Used for refresh scheduling. */
|
|
113
|
+
let tokenTtl = 3600;
|
|
114
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Bootstrap auth via Unix socket auto-approve.
|
|
118
|
+
* Returns true if successful.
|
|
119
|
+
*/
|
|
120
|
+
function bootstrapViaSocket(): Promise<boolean> {
|
|
121
|
+
const uid = process.getuid?.() ?? 'unknown';
|
|
122
|
+
const socketPaths = resolveAuraSocketCandidates({
|
|
123
|
+
uid,
|
|
124
|
+
serverUrl: WALLET_BASE(),
|
|
125
|
+
serverPort: process.env.WALLET_SERVER_PORT,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const trySocket = (socketPath: string): Promise<{ ok: boolean; connectionFailure: boolean }> =>
|
|
129
|
+
new Promise((resolve) => {
|
|
130
|
+
const socket = net.createConnection(socketPath);
|
|
131
|
+
let buffer = '';
|
|
132
|
+
let resolved = false;
|
|
133
|
+
|
|
134
|
+
const timeout = setTimeout(() => {
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
resolved = true;
|
|
137
|
+
socket.destroy();
|
|
138
|
+
resolve({ ok: false, connectionFailure: true });
|
|
139
|
+
}
|
|
140
|
+
}, 5000);
|
|
141
|
+
|
|
142
|
+
socket.on('connect', () => {
|
|
143
|
+
socket.write(JSON.stringify({
|
|
144
|
+
type: 'auth',
|
|
145
|
+
agentId: 'mcp-stdio',
|
|
146
|
+
autoApprove: true,
|
|
147
|
+
pubkey: ephemeralPubPem,
|
|
148
|
+
}) + '\n');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
socket.on('data', (data) => {
|
|
152
|
+
buffer += data.toString();
|
|
153
|
+
let newlineIndex;
|
|
154
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
155
|
+
const line = buffer.substring(0, newlineIndex);
|
|
156
|
+
buffer = buffer.substring(newlineIndex + 1);
|
|
157
|
+
|
|
158
|
+
if (!line.trim()) continue;
|
|
159
|
+
try {
|
|
160
|
+
const msg = JSON.parse(line.trim()) as {
|
|
161
|
+
type: string;
|
|
162
|
+
encryptedToken?: string;
|
|
163
|
+
ttl?: number;
|
|
164
|
+
message?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (msg.type === 'auth_approved' && msg.encryptedToken) {
|
|
168
|
+
token = decryptWithPrivateKey(msg.encryptedToken, ephemeralPrivPem);
|
|
169
|
+
tokenTtl = msg.ttl || 3600;
|
|
170
|
+
if (!resolved) {
|
|
171
|
+
resolved = true;
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
socket.destroy();
|
|
174
|
+
console.error(`[mcp] Bootstrapped via Unix socket (auto-approve path: ${socketPath})`);
|
|
175
|
+
resolve({ ok: true, connectionFailure: false });
|
|
176
|
+
}
|
|
177
|
+
} else if (msg.type === 'error') {
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
resolved = true;
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
socket.destroy();
|
|
182
|
+
console.error(`[mcp] Socket bootstrap error: ${msg.message}`);
|
|
183
|
+
resolve({ ok: false, connectionFailure: false });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch { /* ignore parse errors */ }
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
socket.on('error', (err) => {
|
|
191
|
+
if (!resolved) {
|
|
192
|
+
resolved = true;
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
console.error(`[mcp] Socket bootstrap connection error: ${err.message} (code=${(err as NodeJS.ErrnoException).code})`);
|
|
195
|
+
resolve({ ok: false, connectionFailure: true });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return (async () => {
|
|
201
|
+
for (const socketPath of socketPaths) {
|
|
202
|
+
const result = await trySocket(socketPath);
|
|
203
|
+
if (result.ok) return true;
|
|
204
|
+
if (!result.connectionFailure) return false;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Schedule a token refresh before expiry.
|
|
212
|
+
*/
|
|
213
|
+
function scheduleRefresh(): void {
|
|
214
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
215
|
+
// Refresh 60s before expiry, minimum 10s
|
|
216
|
+
const refreshMs = Math.max((tokenTtl - 60) * 1000, 10_000);
|
|
217
|
+
refreshTimer = setTimeout(() => attemptRefresh(0), refreshMs);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Retry token refresh with exponential backoff (30s, 60s, 120s, 240s, cap 300s). */
|
|
221
|
+
async function attemptRefresh(attempt: number): Promise<void> {
|
|
222
|
+
console.error(`[mcp] Refreshing token (attempt ${attempt + 1})...`);
|
|
223
|
+
const ok = await bootstrapViaSocket();
|
|
224
|
+
if (ok) {
|
|
225
|
+
scheduleRefresh();
|
|
226
|
+
} else {
|
|
227
|
+
const backoffMs = Math.min(30_000 * Math.pow(2, attempt), 300_000); // cap at 5 min
|
|
228
|
+
console.error(`[mcp] Token refresh failed — retrying in ${backoffMs / 1000}s`);
|
|
229
|
+
refreshTimer = setTimeout(() => attemptRefresh(attempt + 1), backoffMs);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── MCP Server Setup ───────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
const server = new McpServer({
|
|
236
|
+
name: 'auramaxx',
|
|
237
|
+
version: '1.0.0',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── Resources ──────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function loadApiDocs(): string {
|
|
243
|
+
try { return readFileSync(join(__dirname, '..', '..', 'docs', 'API.md'), 'utf-8'); }
|
|
244
|
+
catch { return 'API documentation not found.'; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function loadAuthDocs(): string {
|
|
248
|
+
try { return readFileSync(join(__dirname, '..', '..', 'docs', 'AUTH.md'), 'utf-8'); }
|
|
249
|
+
catch { return 'Auth documentation not found.'; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function loadAgentGuide(): string {
|
|
253
|
+
try { return readFileSync(join(__dirname, '..', '..', 'skills', 'auramaxx', 'SKILL.md'), 'utf-8'); }
|
|
254
|
+
catch { return 'Agent guide not found.'; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
server.resource(
|
|
258
|
+
'api-reference', 'docs://api',
|
|
259
|
+
{ description: 'Full AuraMaxx HTTP API reference — all endpoints, parameters, and examples' },
|
|
260
|
+
async () => ({ contents: [{ uri: 'docs://api', text: loadApiDocs(), mimeType: 'text/markdown' }] }),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
server.resource(
|
|
264
|
+
'auth-reference', 'docs://auth',
|
|
265
|
+
{ description: 'Authentication, permissions, and spending limits reference' },
|
|
266
|
+
async () => ({ contents: [{ uri: 'docs://auth', text: loadAuthDocs(), mimeType: 'text/markdown' }] }),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
server.resource(
|
|
270
|
+
'agent-guide', 'docs://guide',
|
|
271
|
+
{ description: 'Agent skill reference — setup, operations, hook modes, permissions, error recovery' },
|
|
272
|
+
async () => ({ contents: [{ uri: 'docs://guide', text: loadAgentGuide(), mimeType: 'text/markdown' }] }),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// ── Credential helpers ─────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Decrypt a credential payload encrypted to our ephemeral RSA key.
|
|
279
|
+
*/
|
|
280
|
+
async function fetchVaultNameMap(): Promise<Map<string, string>> {
|
|
281
|
+
const res = await fetch(`${WALLET_BASE()}/setup/vaults`, {
|
|
282
|
+
signal: AbortSignal.timeout(5000),
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) return new Map();
|
|
285
|
+
const data = await res.json() as { vaults?: Array<{ id: string; name: string }> };
|
|
286
|
+
return new Map((data.vaults || []).map((v) => [v.id, v.name]));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const PROJECT_SCOPE_MODE_CACHE_MS = 10_000;
|
|
290
|
+
let cachedProjectScopeMode: ProjectScopeMode = 'auto';
|
|
291
|
+
let cachedProjectScopeModeAt = 0;
|
|
292
|
+
|
|
293
|
+
function normalizeProjectScopeMode(raw: unknown): ProjectScopeMode {
|
|
294
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
295
|
+
if (value === 'strict') return 'strict';
|
|
296
|
+
if (value === 'off') return 'off';
|
|
297
|
+
return 'auto';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function fetchProjectScopeMode(): Promise<ProjectScopeMode> {
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
if (now - cachedProjectScopeModeAt < PROJECT_SCOPE_MODE_CACHE_MS) {
|
|
303
|
+
return cachedProjectScopeMode;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch(`${WALLET_BASE()}/setup`, {
|
|
307
|
+
signal: AbortSignal.timeout(5000),
|
|
308
|
+
});
|
|
309
|
+
if (!res.ok) return cachedProjectScopeMode;
|
|
310
|
+
const data = await res.json() as { projectScopeMode?: unknown };
|
|
311
|
+
cachedProjectScopeMode = normalizeProjectScopeMode(data.projectScopeMode);
|
|
312
|
+
cachedProjectScopeModeAt = now;
|
|
313
|
+
return cachedProjectScopeMode;
|
|
314
|
+
} catch {
|
|
315
|
+
return cachedProjectScopeMode;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function decryptCredentialPayload(encryptedBase64: string): {
|
|
320
|
+
id: string;
|
|
321
|
+
vaultId: string;
|
|
322
|
+
type: string;
|
|
323
|
+
fields: Array<{ key: string; value: string; type?: string; sensitive?: boolean }>;
|
|
324
|
+
} {
|
|
325
|
+
const plaintext = decryptWithPrivateKey(encryptedBase64, ephemeralPrivPem);
|
|
326
|
+
return JSON.parse(plaintext);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function extractErrorMessage(raw: string): string {
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(raw) as { error?: unknown };
|
|
332
|
+
if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error;
|
|
333
|
+
} catch { /* plain-text payload */ }
|
|
334
|
+
return raw;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function extractExistingApproval(raw: string): Record<string, unknown> | null {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(raw);
|
|
340
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
341
|
+
const asRecord = parsed as Record<string, unknown>;
|
|
342
|
+
if (asRecord.requiresHumanApproval !== true) return null;
|
|
343
|
+
if (typeof asRecord.requestId !== 'string' || !asRecord.requestId) return null;
|
|
344
|
+
return asRecord;
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildPermissionEscalationResponse(input: {
|
|
351
|
+
operation: string;
|
|
352
|
+
status: number;
|
|
353
|
+
rawBody: string;
|
|
354
|
+
permissions: string[];
|
|
355
|
+
endpoint: string;
|
|
356
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
357
|
+
body?: Record<string, unknown>;
|
|
358
|
+
}): { content: Array<{ type: 'text'; text: string }> } {
|
|
359
|
+
const existingApproval = extractExistingApproval(input.rawBody);
|
|
360
|
+
if (existingApproval) {
|
|
361
|
+
return {
|
|
362
|
+
content: [{
|
|
363
|
+
type: 'text' as const,
|
|
364
|
+
text: JSON.stringify(existingApproval),
|
|
365
|
+
}],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const reason = extractErrorMessage(input.rawBody);
|
|
370
|
+
const dashboardBase = `http://localhost:${process.env.DASHBOARD_PORT || '4747'}`;
|
|
371
|
+
return {
|
|
372
|
+
content: [{
|
|
373
|
+
type: 'text' as const,
|
|
374
|
+
text: JSON.stringify({
|
|
375
|
+
error: `${input.operation} failed (${input.status}): permission denied`,
|
|
376
|
+
escalated: true,
|
|
377
|
+
requiresHumanApproval: true,
|
|
378
|
+
reason,
|
|
379
|
+
approveAt: dashboardBase,
|
|
380
|
+
nextStep: {
|
|
381
|
+
tool: 'api',
|
|
382
|
+
summary: input.operation,
|
|
383
|
+
permissions: input.permissions,
|
|
384
|
+
action: {
|
|
385
|
+
endpoint: input.endpoint,
|
|
386
|
+
method: input.method,
|
|
387
|
+
...(input.body ? { body: input.body } : {}),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
authFallback: {
|
|
391
|
+
endpoint: '/auth',
|
|
392
|
+
note: 'Request a new token via POST /auth with a profile that includes the required permissions.',
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
}],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function resolveDefaultVaultId(explicitVault?: string): Promise<string> {
|
|
400
|
+
if (explicitVault) return explicitVault;
|
|
401
|
+
try {
|
|
402
|
+
const { listVaults } = await import('../lib/cold');
|
|
403
|
+
const vaults = listVaults();
|
|
404
|
+
const agentVault = vaults.find(v => v.name === 'agent');
|
|
405
|
+
return agentVault ? agentVault.id : 'primary';
|
|
406
|
+
} catch {
|
|
407
|
+
return 'primary';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Resolve the vault for diary entries with fallback chain:
|
|
413
|
+
* 1. daily-logs vault (dedicated diary storage)
|
|
414
|
+
* 2. agent vault (general agent storage)
|
|
415
|
+
* 3. primary vault (last resort)
|
|
416
|
+
*/
|
|
417
|
+
async function resolveDiaryVaultId(explicitVault?: string): Promise<string> {
|
|
418
|
+
if (explicitVault) return explicitVault;
|
|
419
|
+
try {
|
|
420
|
+
const { listVaults, DAILY_LOGS_VAULT_NAME, AGENT_VAULT_NAME } = await import('../lib/cold');
|
|
421
|
+
const vaults = listVaults();
|
|
422
|
+
const dailyLogsVault = vaults.find(v => v.name === DAILY_LOGS_VAULT_NAME);
|
|
423
|
+
if (dailyLogsVault) return dailyLogsVault.id;
|
|
424
|
+
const agentVault = vaults.find(v => v.name === AGENT_VAULT_NAME);
|
|
425
|
+
if (agentVault) return agentVault.id;
|
|
426
|
+
return 'primary';
|
|
427
|
+
} catch {
|
|
428
|
+
return 'primary';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── get_secret rate limiter ─────────────────────────────────────────────
|
|
433
|
+
const GET_SECRET_WINDOW_MS = 60_000; // 1 minute
|
|
434
|
+
const GET_SECRET_MAX = 10; // max 10 requests per window
|
|
435
|
+
const getSecretRequests: number[] = [];
|
|
436
|
+
|
|
437
|
+
function isGetSecretRateLimited(): boolean {
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
// Prune old entries
|
|
440
|
+
while (getSecretRequests.length > 0 && getSecretRequests[0] <= now - GET_SECRET_WINDOW_MS) {
|
|
441
|
+
getSecretRequests.shift();
|
|
442
|
+
}
|
|
443
|
+
if (getSecretRequests.length >= GET_SECRET_MAX) return true;
|
|
444
|
+
getSecretRequests.push(now);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Shared credential resolver ──────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Search for a credential by name or tag and return its ID + name.
|
|
452
|
+
* Shared by get_secret, del_secret, inject_secret, share_secret.
|
|
453
|
+
*/
|
|
454
|
+
async function resolveCredentialByName(
|
|
455
|
+
name: string,
|
|
456
|
+
): Promise<
|
|
457
|
+
| { credentialId: string; credentialName: string }
|
|
458
|
+
| { error: string; escalation?: ReturnType<typeof buildPermissionEscalationResponse> }
|
|
459
|
+
> {
|
|
460
|
+
const base = WALLET_BASE();
|
|
461
|
+
try {
|
|
462
|
+
const vaultNames = await fetchVaultNameMap();
|
|
463
|
+
const projectScopeMode = await fetchProjectScopeMode();
|
|
464
|
+
for (const queryParam of [`q=${encodeURIComponent(name)}`, `tag=${encodeURIComponent(name)}`]) {
|
|
465
|
+
const res = await fetch(`${base}/credentials?${queryParam}`, {
|
|
466
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
467
|
+
signal: AbortSignal.timeout(5000),
|
|
468
|
+
});
|
|
469
|
+
if (res.status === 403) {
|
|
470
|
+
const text = await res.text();
|
|
471
|
+
return {
|
|
472
|
+
error: `Permission denied searching for "${name}"`,
|
|
473
|
+
escalation: buildPermissionEscalationResponse({
|
|
474
|
+
operation: `Read secret "${name}"`,
|
|
475
|
+
status: res.status,
|
|
476
|
+
rawBody: text,
|
|
477
|
+
permissions: ['secret:read'],
|
|
478
|
+
endpoint: `/credentials?${queryParam}`,
|
|
479
|
+
method: 'GET',
|
|
480
|
+
}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (!res.ok) continue;
|
|
484
|
+
|
|
485
|
+
const data = await res.json() as { credentials: Array<{ id: string; name: string; vaultId: string }> };
|
|
486
|
+
if (!data.credentials || data.credentials.length === 0) continue;
|
|
487
|
+
|
|
488
|
+
const decision = evaluateProjectScopeAccess({
|
|
489
|
+
surface: 'mcp_get_secret',
|
|
490
|
+
requested: { vaultName: null, credentialName: name },
|
|
491
|
+
candidates: data.credentials.map((c) => ({
|
|
492
|
+
id: c.id,
|
|
493
|
+
name: c.name,
|
|
494
|
+
vaultName: vaultNames.get(c.vaultId) || null,
|
|
495
|
+
})),
|
|
496
|
+
actor: 'mcp-stdio',
|
|
497
|
+
projectScopeMode,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
emitProjectScopeEvent({
|
|
501
|
+
actor: 'mcp-stdio',
|
|
502
|
+
surface: 'mcp_get_secret',
|
|
503
|
+
requestedCredential: { vaultName: null, credentialName: name },
|
|
504
|
+
decision,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (!decision.allowed) {
|
|
508
|
+
return { error: `${decision.code}: ${decision.remediation}` };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const allowedIds = new Set(decision.allowedCandidates.map((c) => c.id).filter(Boolean));
|
|
512
|
+
const scopedCandidates = data.credentials.filter((c) => allowedIds.has(c.id));
|
|
513
|
+
if (scopedCandidates.length === 0) continue;
|
|
514
|
+
|
|
515
|
+
return { credentialId: scopedCandidates[0].id, credentialName: scopedCandidates[0].name };
|
|
516
|
+
}
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return { error: `Search failed: ${err}` };
|
|
519
|
+
}
|
|
520
|
+
return { error: `No credential found matching "${name}"` };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── get_secret ─────────────────────────────────────────────────────────
|
|
524
|
+
server.tool(
|
|
525
|
+
'get_secret',
|
|
526
|
+
'Look up a stored credential/secret by name or tag and return its decrypted value. Searches credential vaults for a matching entry and returns all fields (including sensitive ones like passwords, API keys, etc.).',
|
|
527
|
+
{ name: z.string().describe('Name or tag to search for (e.g. "GitHub", "openai", "deploy")') },
|
|
528
|
+
async (input) => {
|
|
529
|
+
const { name } = input as { name: string };
|
|
530
|
+
|
|
531
|
+
if (isGetSecretRateLimited()) {
|
|
532
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Rate limited — too many get_secret requests. Try again in 1 minute.' }) }] };
|
|
533
|
+
}
|
|
534
|
+
if (!token) {
|
|
535
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const resolved = await resolveCredentialByName(name);
|
|
539
|
+
if ('error' in resolved) {
|
|
540
|
+
if ('escalation' in resolved && resolved.escalation) return resolved.escalation;
|
|
541
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: resolved.error }) }] };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const { credentialId, credentialName } = resolved;
|
|
545
|
+
const base = WALLET_BASE();
|
|
546
|
+
const readToken = token;
|
|
547
|
+
|
|
548
|
+
// Step 3: Read credential (encrypted to our ephemeral key)
|
|
549
|
+
try {
|
|
550
|
+
const res = await fetch(`${base}/credentials/${credentialId}/read`, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: {
|
|
553
|
+
'Authorization': `Bearer ${readToken}`,
|
|
554
|
+
'X-Secret-Surface': 'get_secret',
|
|
555
|
+
'X-Credential-Name': credentialName || name,
|
|
556
|
+
},
|
|
557
|
+
signal: AbortSignal.timeout(5000),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (!res.ok) {
|
|
561
|
+
const text = await res.text();
|
|
562
|
+
if (res.status === 403) {
|
|
563
|
+
return buildPermissionEscalationResponse({
|
|
564
|
+
operation: `Read secret "${credentialName || name}"`,
|
|
565
|
+
status: res.status,
|
|
566
|
+
rawBody: text,
|
|
567
|
+
permissions: ['secret:read'],
|
|
568
|
+
endpoint: `/credentials/${credentialId}/read`,
|
|
569
|
+
method: 'POST',
|
|
570
|
+
body: {},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Read failed (${res.status}): ${text}` }) }] };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const data = await res.json() as { encrypted: string };
|
|
577
|
+
const decrypted = decryptCredentialPayload(data.encrypted);
|
|
578
|
+
|
|
579
|
+
// If credential has a TOTP field, generate current code
|
|
580
|
+
const totpField = decrypted.fields?.find((f: { key: string }) => f.key === 'totp' || f.key === 'otp');
|
|
581
|
+
let totpCode: string | undefined;
|
|
582
|
+
let totpRemaining: number | undefined;
|
|
583
|
+
if (totpField) {
|
|
584
|
+
try {
|
|
585
|
+
const totpRes = await fetch(`${base}/credentials/${credentialId}/totp`, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
588
|
+
signal: AbortSignal.timeout(5000),
|
|
589
|
+
});
|
|
590
|
+
if (totpRes.ok) {
|
|
591
|
+
const totpData = await totpRes.json() as { code: string; remaining: number };
|
|
592
|
+
totpCode = totpData.code;
|
|
593
|
+
totpRemaining = totpData.remaining;
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
// TOTP generation failed — skip, still return credential
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
content: [{
|
|
602
|
+
type: 'text' as const,
|
|
603
|
+
text: JSON.stringify({
|
|
604
|
+
success: true,
|
|
605
|
+
name: credentialName,
|
|
606
|
+
credentialId: decrypted.id,
|
|
607
|
+
type: decrypted.type,
|
|
608
|
+
fields: decrypted.fields,
|
|
609
|
+
...(totpCode && { totpCode, totpRemaining }),
|
|
610
|
+
}),
|
|
611
|
+
}],
|
|
612
|
+
};
|
|
613
|
+
} catch (err) {
|
|
614
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Decryption failed: ${err}` }) }] };
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// ── put_secret ─────────────────────────────────────────────────────────
|
|
620
|
+
server.tool(
|
|
621
|
+
'put_secret',
|
|
622
|
+
'Store a new credential/secret with the given name and value. Creates a "note" type credential in the default vault with a single sensitive field.',
|
|
623
|
+
{
|
|
624
|
+
name: z.string().describe('Name for the credential (e.g. "OpenAI API Key", "GitHub Token")'),
|
|
625
|
+
value: z.string().describe('The secret value to store'),
|
|
626
|
+
vault: z.string().optional().describe('Vault ID to store in (defaults to "agent", falls back to "primary")'),
|
|
627
|
+
tags: z.array(z.string()).optional().describe('Optional tags for organization'),
|
|
628
|
+
},
|
|
629
|
+
async (input) => {
|
|
630
|
+
const { name, value, vault, tags } = input as { name: string; value: string; vault?: string; tags?: string[] };
|
|
631
|
+
|
|
632
|
+
if (!token) {
|
|
633
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const base = WALLET_BASE();
|
|
637
|
+
const vaultId = await resolveDefaultVaultId(vault);
|
|
638
|
+
const createBody = {
|
|
639
|
+
vaultId,
|
|
640
|
+
type: 'note',
|
|
641
|
+
name,
|
|
642
|
+
meta: tags ? { tags } : {},
|
|
643
|
+
fields: [{ key: NOTE_CONTENT_KEY, value, type: 'secret', sensitive: true }],
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const res = await fetch(`${base}/credentials`, {
|
|
648
|
+
method: 'POST',
|
|
649
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
650
|
+
body: JSON.stringify(createBody),
|
|
651
|
+
signal: AbortSignal.timeout(5000),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const text = await res.text();
|
|
655
|
+
if (!res.ok) {
|
|
656
|
+
if (res.status === 403) {
|
|
657
|
+
return buildPermissionEscalationResponse({
|
|
658
|
+
operation: `Store secret "${name}"`,
|
|
659
|
+
status: res.status,
|
|
660
|
+
rawBody: text,
|
|
661
|
+
permissions: ['secret:write'],
|
|
662
|
+
endpoint: '/credentials',
|
|
663
|
+
method: 'POST',
|
|
664
|
+
body: createBody,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Store failed (${res.status}): ${text}` }) }] };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const data = JSON.parse(text) as { credential: { id: string; name: string } };
|
|
671
|
+
return {
|
|
672
|
+
content: [{
|
|
673
|
+
type: 'text' as const,
|
|
674
|
+
text: JSON.stringify({
|
|
675
|
+
success: true,
|
|
676
|
+
credentialId: data.credential.id,
|
|
677
|
+
name: data.credential.name,
|
|
678
|
+
message: `Secret "${name}" stored successfully`,
|
|
679
|
+
}),
|
|
680
|
+
}],
|
|
681
|
+
};
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Store failed: ${err}` }) }] };
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// ── write_diary ────────────────────────────────────────────────────────
|
|
689
|
+
server.tool(
|
|
690
|
+
'write_diary',
|
|
691
|
+
'Append an entry to a daily diary note (YYYY-MM-DD_LOGS). Uses the daily-logs vault when available, falls back to agent vault, then primary vault.',
|
|
692
|
+
{
|
|
693
|
+
entry: z.string().min(1).describe('Diary text to append'),
|
|
694
|
+
date: z.string().optional().describe('Optional date in YYYY-MM-DD (defaults to today UTC)'),
|
|
695
|
+
},
|
|
696
|
+
async (input) => {
|
|
697
|
+
const { entry, date } = input as { entry: string; date?: string };
|
|
698
|
+
|
|
699
|
+
if (!token) {
|
|
700
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
701
|
+
}
|
|
702
|
+
if (!entry.trim()) {
|
|
703
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'entry must not be empty' }) }] };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const resolvedDate = resolveDiaryDate(date);
|
|
707
|
+
if (!resolvedDate) {
|
|
708
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'date must be YYYY-MM-DD' }) }] };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const base = WALLET_BASE();
|
|
712
|
+
const vaultId = await resolveDiaryVaultId();
|
|
713
|
+
const diaryName = getDiaryCredentialName(resolvedDate);
|
|
714
|
+
const legacyDiaryName = getLegacyDiaryCredentialName(resolvedDate);
|
|
715
|
+
const entryBlock = formatDiaryEntry(entry);
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const listRes = await fetch(
|
|
719
|
+
`${base}/credentials?vault=${encodeURIComponent(vaultId)}&q=${encodeURIComponent(resolvedDate)}`,
|
|
720
|
+
{
|
|
721
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
722
|
+
signal: AbortSignal.timeout(5000),
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (!listRes.ok) {
|
|
727
|
+
const text = await listRes.text();
|
|
728
|
+
if (listRes.status === 403) {
|
|
729
|
+
return buildPermissionEscalationResponse({
|
|
730
|
+
operation: `Read diary "${diaryName}"`,
|
|
731
|
+
status: listRes.status,
|
|
732
|
+
rawBody: text,
|
|
733
|
+
permissions: ['secret:read'],
|
|
734
|
+
endpoint: `/credentials?vault=${encodeURIComponent(vaultId)}&q=${encodeURIComponent(resolvedDate)}`,
|
|
735
|
+
method: 'GET',
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Diary lookup failed (${listRes.status}): ${text}` }) }] };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const listed = await listRes.json() as {
|
|
742
|
+
credentials?: Array<{ id: string; name: string; vaultId: string }>;
|
|
743
|
+
};
|
|
744
|
+
const existing = (listed.credentials || []).find((c) => c.name === diaryName)
|
|
745
|
+
|| (listed.credentials || []).find((c) => c.name === legacyDiaryName);
|
|
746
|
+
|
|
747
|
+
if (!existing) {
|
|
748
|
+
const createBody = {
|
|
749
|
+
vaultId,
|
|
750
|
+
type: 'note',
|
|
751
|
+
name: diaryName,
|
|
752
|
+
meta: { tags: ['diary', 'heartbeat'] },
|
|
753
|
+
fields: [{ key: NOTE_CONTENT_KEY, value: entryBlock, type: 'secret', sensitive: true }],
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const createRes = await fetch(`${base}/credentials`, {
|
|
757
|
+
method: 'POST',
|
|
758
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
759
|
+
body: JSON.stringify(createBody),
|
|
760
|
+
signal: AbortSignal.timeout(5000),
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const createText = await createRes.text();
|
|
764
|
+
if (!createRes.ok) {
|
|
765
|
+
if (createRes.status === 403) {
|
|
766
|
+
return buildPermissionEscalationResponse({
|
|
767
|
+
operation: `Create diary "${diaryName}"`,
|
|
768
|
+
status: createRes.status,
|
|
769
|
+
rawBody: createText,
|
|
770
|
+
permissions: ['secret:write'],
|
|
771
|
+
endpoint: '/credentials',
|
|
772
|
+
method: 'POST',
|
|
773
|
+
body: createBody,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Diary create failed (${createRes.status}): ${createText}` }) }] };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const created = JSON.parse(createText) as { credential: { id: string } };
|
|
780
|
+
return {
|
|
781
|
+
content: [{
|
|
782
|
+
type: 'text' as const,
|
|
783
|
+
text: JSON.stringify({
|
|
784
|
+
success: true,
|
|
785
|
+
date: resolvedDate,
|
|
786
|
+
entryCount: 1,
|
|
787
|
+
credentialId: created.credential.id,
|
|
788
|
+
vaultId,
|
|
789
|
+
}),
|
|
790
|
+
}],
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const readRes = await fetch(`${base}/credentials/${existing.id}/read`, {
|
|
795
|
+
method: 'POST',
|
|
796
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
797
|
+
signal: AbortSignal.timeout(5000),
|
|
798
|
+
});
|
|
799
|
+
if (!readRes.ok) {
|
|
800
|
+
const text = await readRes.text();
|
|
801
|
+
if (readRes.status === 403) {
|
|
802
|
+
return buildPermissionEscalationResponse({
|
|
803
|
+
operation: `Read diary "${existing.name}"`,
|
|
804
|
+
status: readRes.status,
|
|
805
|
+
rawBody: text,
|
|
806
|
+
permissions: ['secret:read', 'secret:write'],
|
|
807
|
+
endpoint: `/credentials/${existing.id}/read`,
|
|
808
|
+
method: 'POST',
|
|
809
|
+
body: {},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Diary read failed (${readRes.status}): ${text}` }) }] };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const readData = await readRes.json() as {
|
|
816
|
+
encrypted?: string;
|
|
817
|
+
fields?: Array<{ key: string; value: string; type?: string; sensitive?: boolean }>;
|
|
818
|
+
};
|
|
819
|
+
const decrypted = readData.encrypted
|
|
820
|
+
? decryptCredentialPayload(readData.encrypted)
|
|
821
|
+
: {
|
|
822
|
+
id: existing.id,
|
|
823
|
+
vaultId: existing.vaultId,
|
|
824
|
+
type: 'note',
|
|
825
|
+
fields: readData.fields || [],
|
|
826
|
+
};
|
|
827
|
+
const previousText = getCredentialFieldValue('note', decrypted.fields, NOTE_CONTENT_KEY)
|
|
828
|
+
|| decrypted.fields[0]?.value
|
|
829
|
+
|| '';
|
|
830
|
+
const nextText = appendDiaryEntry(previousText, entryBlock);
|
|
831
|
+
const updateBody = {
|
|
832
|
+
name: diaryName,
|
|
833
|
+
sensitiveFields: [{ key: NOTE_CONTENT_KEY, value: nextText, type: 'secret', sensitive: true }],
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const updateRes = await fetch(`${base}/credentials/${existing.id}`, {
|
|
837
|
+
method: 'PUT',
|
|
838
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
839
|
+
body: JSON.stringify(updateBody),
|
|
840
|
+
signal: AbortSignal.timeout(5000),
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const updateText = await updateRes.text();
|
|
844
|
+
if (!updateRes.ok) {
|
|
845
|
+
if (updateRes.status === 403) {
|
|
846
|
+
return buildPermissionEscalationResponse({
|
|
847
|
+
operation: `Update diary "${diaryName}"`,
|
|
848
|
+
status: updateRes.status,
|
|
849
|
+
rawBody: updateText,
|
|
850
|
+
permissions: ['secret:read', 'secret:write'],
|
|
851
|
+
endpoint: `/credentials/${existing.id}`,
|
|
852
|
+
method: 'PUT',
|
|
853
|
+
body: updateBody,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Diary update failed (${updateRes.status}): ${updateText}` }) }] };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
content: [{
|
|
861
|
+
type: 'text' as const,
|
|
862
|
+
text: JSON.stringify({
|
|
863
|
+
success: true,
|
|
864
|
+
date: resolvedDate,
|
|
865
|
+
name: diaryName,
|
|
866
|
+
entryCount: countDiaryEntries(nextText),
|
|
867
|
+
credentialId: existing.id,
|
|
868
|
+
vaultId: existing.vaultId || vaultId,
|
|
869
|
+
}),
|
|
870
|
+
}],
|
|
871
|
+
};
|
|
872
|
+
} catch (err) {
|
|
873
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `write_diary failed: ${err}` }) }] };
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// ── del_secret ─────────────────────────────────────────────────────────
|
|
879
|
+
server.tool(
|
|
880
|
+
'del_secret',
|
|
881
|
+
'Delete a stored credential/secret by name. Searches for the credential then deletes it from the active vault.',
|
|
882
|
+
{
|
|
883
|
+
name: z.string().describe('Name or tag of the credential to delete'),
|
|
884
|
+
location: z.enum(['active', 'archive', 'recently_deleted']).optional().describe('Credential location (default: active)'),
|
|
885
|
+
},
|
|
886
|
+
async (input) => {
|
|
887
|
+
const { name, location } = input as { name: string; location?: string };
|
|
888
|
+
|
|
889
|
+
if (!token) {
|
|
890
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const resolved = await resolveCredentialByName(name);
|
|
894
|
+
if ('error' in resolved) {
|
|
895
|
+
if ('escalation' in resolved && resolved.escalation) return resolved.escalation;
|
|
896
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: resolved.error }) }] };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const base = WALLET_BASE();
|
|
900
|
+
const loc = location || 'active';
|
|
901
|
+
const qs = new URLSearchParams({ location: loc }).toString();
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
const res = await fetch(`${base}/credentials/${encodeURIComponent(resolved.credentialId)}?${qs}`, {
|
|
905
|
+
method: 'DELETE',
|
|
906
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
907
|
+
signal: AbortSignal.timeout(8000),
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const text = await res.text();
|
|
911
|
+
if (!res.ok) {
|
|
912
|
+
if (res.status === 403) {
|
|
913
|
+
return buildPermissionEscalationResponse({
|
|
914
|
+
operation: `Delete secret "${resolved.credentialName}"`,
|
|
915
|
+
status: res.status,
|
|
916
|
+
rawBody: text,
|
|
917
|
+
permissions: ['secret:write'],
|
|
918
|
+
endpoint: `/credentials/${resolved.credentialId}`,
|
|
919
|
+
method: 'DELETE',
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Delete failed (${res.status}): ${text}` }) }] };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let data: Record<string, unknown> = {};
|
|
926
|
+
try { data = JSON.parse(text); } catch { /* plain text */ }
|
|
927
|
+
return {
|
|
928
|
+
content: [{
|
|
929
|
+
type: 'text' as const,
|
|
930
|
+
text: JSON.stringify({
|
|
931
|
+
success: true,
|
|
932
|
+
name: resolved.credentialName,
|
|
933
|
+
credentialId: resolved.credentialId,
|
|
934
|
+
action: data.action || 'deleted',
|
|
935
|
+
}),
|
|
936
|
+
}],
|
|
937
|
+
};
|
|
938
|
+
} catch (err) {
|
|
939
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `del_secret failed: ${err}` }) }] };
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// ── inject_secret ──────────────────────────────────────────────────────
|
|
945
|
+
server.tool(
|
|
946
|
+
'inject_secret',
|
|
947
|
+
'Look up a credential by name, extract its primary secret field, and either set it as a process env var or spawn a child command with the env var injected. Use stdio pipes to avoid MCP interference.',
|
|
948
|
+
{
|
|
949
|
+
name: z.string().describe('Name or tag of the credential to inject'),
|
|
950
|
+
envVar: z.string().describe('Environment variable name to set (e.g. "OPENAI_API_KEY")'),
|
|
951
|
+
command: z.array(z.string()).optional().describe('Optional command + args to spawn with the env var (e.g. ["node", "script.js"]). If omitted, sets the env var in the MCP server process.'),
|
|
952
|
+
},
|
|
953
|
+
async (input) => {
|
|
954
|
+
const { name, envVar, command } = input as { name: string; envVar: string; command?: string[] };
|
|
955
|
+
|
|
956
|
+
if (!token) {
|
|
957
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (isGetSecretRateLimited()) {
|
|
961
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Rate limited — too many secret requests. Try again in 1 minute.' }) }] };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const resolved = await resolveCredentialByName(name);
|
|
965
|
+
if ('error' in resolved) {
|
|
966
|
+
if ('escalation' in resolved && resolved.escalation) return resolved.escalation;
|
|
967
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: resolved.error }) }] };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const base = WALLET_BASE();
|
|
971
|
+
|
|
972
|
+
// Read + decrypt credential
|
|
973
|
+
let secretValue: string;
|
|
974
|
+
try {
|
|
975
|
+
const res = await fetch(`${base}/credentials/${resolved.credentialId}/read`, {
|
|
976
|
+
method: 'POST',
|
|
977
|
+
headers: {
|
|
978
|
+
'Authorization': `Bearer ${token}`,
|
|
979
|
+
'X-Secret-Surface': 'inject_secret',
|
|
980
|
+
'X-Secret-EnvVar': envVar,
|
|
981
|
+
'X-Credential-Name': resolved.credentialName || name,
|
|
982
|
+
},
|
|
983
|
+
signal: AbortSignal.timeout(5000),
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
if (!res.ok) {
|
|
987
|
+
const text = await res.text();
|
|
988
|
+
if (res.status === 403) {
|
|
989
|
+
return buildPermissionEscalationResponse({
|
|
990
|
+
operation: `Read secret "${resolved.credentialName}" for injection`,
|
|
991
|
+
status: res.status,
|
|
992
|
+
rawBody: text,
|
|
993
|
+
permissions: ['secret:read'],
|
|
994
|
+
endpoint: `/credentials/${resolved.credentialId}/read`,
|
|
995
|
+
method: 'POST',
|
|
996
|
+
body: {},
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Read failed (${res.status}): ${text}` }) }] };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const data = await res.json() as { encrypted: string };
|
|
1003
|
+
const decrypted = decryptCredentialPayload(data.encrypted);
|
|
1004
|
+
|
|
1005
|
+
// Extract primary secret field
|
|
1006
|
+
const noteField = getCredentialFieldValue('note', decrypted.fields, NOTE_CONTENT_KEY);
|
|
1007
|
+
const passwordField = decrypted.fields.find((f: { key: string; sensitive?: boolean }) => f.sensitive)?.value;
|
|
1008
|
+
secretValue = noteField || passwordField || decrypted.fields[0]?.value || '';
|
|
1009
|
+
|
|
1010
|
+
if (!secretValue) {
|
|
1011
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Credential "${name}" has no extractable secret value` }) }] };
|
|
1012
|
+
}
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `inject_secret read failed: ${err}` }) }] };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Set env var or spawn child
|
|
1018
|
+
if (!command || command.length === 0) {
|
|
1019
|
+
process.env[envVar] = secretValue;
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{
|
|
1022
|
+
type: 'text' as const,
|
|
1023
|
+
text: JSON.stringify({
|
|
1024
|
+
success: true,
|
|
1025
|
+
envVar,
|
|
1026
|
+
name: resolved.credentialName,
|
|
1027
|
+
scope: 'mcp-server-process',
|
|
1028
|
+
message: `Set ${envVar} in MCP server process. Use command param to inject into a child process.`,
|
|
1029
|
+
}),
|
|
1030
|
+
}],
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Spawn child process with injected env var (use pipe stdio to avoid MCP interference)
|
|
1035
|
+
return new Promise((resolve) => {
|
|
1036
|
+
const child = spawn(command[0], command.slice(1), {
|
|
1037
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1038
|
+
env: { ...process.env, [envVar]: secretValue },
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
let stdout = '';
|
|
1042
|
+
let stderr = '';
|
|
1043
|
+
|
|
1044
|
+
child.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
1045
|
+
child.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
1046
|
+
|
|
1047
|
+
child.on('error', (error) => {
|
|
1048
|
+
resolve({
|
|
1049
|
+
content: [{
|
|
1050
|
+
type: 'text' as const,
|
|
1051
|
+
text: JSON.stringify({ error: `Failed to execute command: ${error.message}` }),
|
|
1052
|
+
}],
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
child.on('exit', (code) => {
|
|
1057
|
+
const truncated = (s: string) => s.length > 2000 ? s.slice(0, 2000) + '...[truncated]' : s;
|
|
1058
|
+
resolve({
|
|
1059
|
+
content: [{
|
|
1060
|
+
type: 'text' as const,
|
|
1061
|
+
text: JSON.stringify({
|
|
1062
|
+
success: code === 0,
|
|
1063
|
+
exitCode: code,
|
|
1064
|
+
envVar,
|
|
1065
|
+
command: command.join(' '),
|
|
1066
|
+
stdout: truncated(stdout),
|
|
1067
|
+
...(stderr ? { stderr: truncated(stderr) } : {}),
|
|
1068
|
+
}),
|
|
1069
|
+
}],
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
},
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
// ── share_secret ───────────────────────────────────────────────────────
|
|
1077
|
+
server.tool(
|
|
1078
|
+
'share_secret',
|
|
1079
|
+
'Create a shareable link for a credential. Resolves by name, then creates a time-limited share URL with optional password protection.',
|
|
1080
|
+
{
|
|
1081
|
+
name: z.string().describe('Name or tag of the credential to share'),
|
|
1082
|
+
expiresAfter: z.string().optional().describe('Expiry duration (e.g. "1h", "7d", "30m"). Default: server default.'),
|
|
1083
|
+
accessMode: z.enum(['anyone', 'password']).optional().describe('Access mode: "anyone" (link only) or "password" (requires password)'),
|
|
1084
|
+
password: z.string().optional().describe('Password for password-protected shares'),
|
|
1085
|
+
oneTimeOnly: z.boolean().optional().describe('If true, share link can only be viewed once'),
|
|
1086
|
+
},
|
|
1087
|
+
async (input) => {
|
|
1088
|
+
const { name, expiresAfter, accessMode, password, oneTimeOnly } = input as {
|
|
1089
|
+
name: string;
|
|
1090
|
+
expiresAfter?: string;
|
|
1091
|
+
accessMode?: 'anyone' | 'password';
|
|
1092
|
+
password?: string;
|
|
1093
|
+
oneTimeOnly?: boolean;
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
if (!token) {
|
|
1097
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No auth token — start AuraMaxx server for auto-bootstrap, or set AURA_TOKEN env var' }) }] };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const resolved = await resolveCredentialByName(name);
|
|
1101
|
+
if ('error' in resolved) {
|
|
1102
|
+
if ('escalation' in resolved && resolved.escalation) return resolved.escalation;
|
|
1103
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: resolved.error }) }] };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const base = WALLET_BASE();
|
|
1107
|
+
const shareBody: Record<string, unknown> = { credentialId: resolved.credentialId };
|
|
1108
|
+
if (expiresAfter) shareBody.expiresAfter = expiresAfter;
|
|
1109
|
+
if (accessMode) shareBody.accessMode = accessMode;
|
|
1110
|
+
if (password) shareBody.password = password;
|
|
1111
|
+
if (oneTimeOnly !== undefined) shareBody.oneTimeOnly = oneTimeOnly;
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const res = await fetch(`${base}/credential-shares`, {
|
|
1115
|
+
method: 'POST',
|
|
1116
|
+
headers: {
|
|
1117
|
+
'Authorization': `Bearer ${token}`,
|
|
1118
|
+
'Content-Type': 'application/json',
|
|
1119
|
+
},
|
|
1120
|
+
body: JSON.stringify(shareBody),
|
|
1121
|
+
signal: AbortSignal.timeout(8000),
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
const text = await res.text();
|
|
1125
|
+
if (!res.ok) {
|
|
1126
|
+
if (res.status === 403) {
|
|
1127
|
+
return buildPermissionEscalationResponse({
|
|
1128
|
+
operation: `Share secret "${resolved.credentialName}"`,
|
|
1129
|
+
status: res.status,
|
|
1130
|
+
rawBody: text,
|
|
1131
|
+
permissions: ['secret:read', 'secret:share'],
|
|
1132
|
+
endpoint: '/credential-shares',
|
|
1133
|
+
method: 'POST',
|
|
1134
|
+
body: shareBody,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Share failed (${res.status}): ${text}` }) }] };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const data = JSON.parse(text) as { success?: boolean; share?: Record<string, unknown>; error?: string };
|
|
1141
|
+
if (!data.success || !data.share) {
|
|
1142
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: data.error || 'Share creation returned unexpected response' }) }] };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
content: [{
|
|
1147
|
+
type: 'text' as const,
|
|
1148
|
+
text: JSON.stringify({
|
|
1149
|
+
success: true,
|
|
1150
|
+
name: resolved.credentialName,
|
|
1151
|
+
credentialId: resolved.credentialId,
|
|
1152
|
+
share: data.share,
|
|
1153
|
+
}),
|
|
1154
|
+
}],
|
|
1155
|
+
};
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `share_secret failed: ${err}` }) }] };
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
// ── auth ────────────────────────────────────────────────────────────────
|
|
1163
|
+
server.tool(
|
|
1164
|
+
'auth',
|
|
1165
|
+
'Request an authenticated session token. Sends an auth request to the server with an ephemeral RSA keypair, then polls for human approval. On approval, decrypts and activates the token for this MCP session. Times out after 120s.',
|
|
1166
|
+
// NOTE: Returns immediately with the approval URL. Polling happens in background.
|
|
1167
|
+
{
|
|
1168
|
+
agentId: z.string().optional().describe('Agent identifier (default: "mcp-stdio")'),
|
|
1169
|
+
profile: z.string().optional().describe('Permission profile name to request'),
|
|
1170
|
+
profileVersion: z.string().optional().describe('Profile version'),
|
|
1171
|
+
profileOverrides: z.record(z.unknown()).optional().describe('Profile permission overrides'),
|
|
1172
|
+
action: z.object({
|
|
1173
|
+
endpoint: z.string().describe('API endpoint to call (e.g. "/send")'),
|
|
1174
|
+
method: z.string().describe('HTTP method (e.g. "POST")'),
|
|
1175
|
+
body: z.record(z.unknown()).optional().describe('Request body for the action'),
|
|
1176
|
+
}).optional().describe('Pre-computed action to auto-execute on approval'),
|
|
1177
|
+
},
|
|
1178
|
+
async (input) => {
|
|
1179
|
+
const { agentId, profile, profileVersion, profileOverrides, action } = input as {
|
|
1180
|
+
agentId?: string;
|
|
1181
|
+
profile?: string;
|
|
1182
|
+
profileVersion?: string;
|
|
1183
|
+
profileOverrides?: Record<string, unknown>;
|
|
1184
|
+
action?: { endpoint: string; method: string; body?: Record<string, unknown> };
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
const base = WALLET_BASE();
|
|
1188
|
+
const resolvedAgentId = agentId || 'mcp-stdio';
|
|
1189
|
+
|
|
1190
|
+
// Step 1: Create auth request with our ephemeral pubkey
|
|
1191
|
+
let requestId: string;
|
|
1192
|
+
let secret: string;
|
|
1193
|
+
let approveUrl: string | undefined;
|
|
1194
|
+
try {
|
|
1195
|
+
const authBody: Record<string, unknown> = {
|
|
1196
|
+
agentId: resolvedAgentId,
|
|
1197
|
+
pubkey: ephemeralPubPem,
|
|
1198
|
+
};
|
|
1199
|
+
if (profile) authBody.profile = profile;
|
|
1200
|
+
if (profileVersion) authBody.profileVersion = profileVersion;
|
|
1201
|
+
if (profileOverrides) authBody.profileOverrides = profileOverrides;
|
|
1202
|
+
if (action) authBody.action = action;
|
|
1203
|
+
|
|
1204
|
+
const res = await fetch(`${base}/auth`, {
|
|
1205
|
+
method: 'POST',
|
|
1206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1207
|
+
body: JSON.stringify(authBody),
|
|
1208
|
+
signal: AbortSignal.timeout(10_000),
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
if (!res.ok) {
|
|
1212
|
+
const text = await res.text();
|
|
1213
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Auth request failed (${res.status}): ${text}` }) }] };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const data = await res.json() as { requestId: string; secret: string; approveUrl?: string };
|
|
1217
|
+
requestId = data.requestId;
|
|
1218
|
+
secret = data.secret;
|
|
1219
|
+
if (data.approveUrl) approveUrl = data.approveUrl;
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Auth request failed: ${err}` }) }] };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const dashboardBase = `http://localhost:${process.env.DASHBOARD_PORT || '4747'}`;
|
|
1225
|
+
|
|
1226
|
+
// Step 2: Start background polling, return approval URL immediately
|
|
1227
|
+
const timeoutMs = 120_000;
|
|
1228
|
+
const intervalMs = 3_000;
|
|
1229
|
+
|
|
1230
|
+
// Track pending auth for get_token
|
|
1231
|
+
const resolvedApproveUrl = approveUrl || `${dashboardBase}/approve/${encodeURIComponent(requestId)}`;
|
|
1232
|
+
pendingAuth = { requestId, agentId: resolvedAgentId, status: 'polling', approveUrl: resolvedApproveUrl };
|
|
1233
|
+
|
|
1234
|
+
// Poll in background — token activates silently when approved
|
|
1235
|
+
(async () => {
|
|
1236
|
+
const startedAt = Date.now();
|
|
1237
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
1238
|
+
try {
|
|
1239
|
+
const pollRes = await fetch(
|
|
1240
|
+
`${base}/auth/${encodeURIComponent(requestId)}?secret=${encodeURIComponent(secret)}`,
|
|
1241
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
if (pollRes.status === 410 || pollRes.status === 403) {
|
|
1245
|
+
if (pendingAuth?.requestId === requestId) pendingAuth.status = 'rejected';
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (pollRes.ok) {
|
|
1250
|
+
const pollData = await pollRes.json() as {
|
|
1251
|
+
status?: string;
|
|
1252
|
+
encryptedToken?: string;
|
|
1253
|
+
ttl?: number;
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
if (pollData.status === 'rejected') {
|
|
1257
|
+
if (pendingAuth?.requestId === requestId) pendingAuth.status = 'rejected';
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (pollData.status === 'approved' && pollData.encryptedToken) {
|
|
1262
|
+
try {
|
|
1263
|
+
token = decryptWithPrivateKey(pollData.encryptedToken, ephemeralPrivPem);
|
|
1264
|
+
tokenTtl = pollData.ttl || 3600;
|
|
1265
|
+
scheduleRefresh();
|
|
1266
|
+
if (pendingAuth?.requestId === requestId) pendingAuth.status = 'approved';
|
|
1267
|
+
console.error(`[mcp] Auth approved — token activated for ${resolvedAgentId}`);
|
|
1268
|
+
} catch { /* decryption failed */ }
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch { /* network error — retry */ }
|
|
1273
|
+
|
|
1274
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1275
|
+
}
|
|
1276
|
+
// Timed out
|
|
1277
|
+
if (pendingAuth?.requestId === requestId) pendingAuth.status = 'timeout';
|
|
1278
|
+
})();
|
|
1279
|
+
|
|
1280
|
+
return {
|
|
1281
|
+
content: [{
|
|
1282
|
+
type: 'text' as const,
|
|
1283
|
+
text: JSON.stringify({
|
|
1284
|
+
success: true,
|
|
1285
|
+
message: `Auth request created. Approve at the link below, then retry your operation.`,
|
|
1286
|
+
approveUrl: resolvedApproveUrl,
|
|
1287
|
+
requestId,
|
|
1288
|
+
agentId: resolvedAgentId,
|
|
1289
|
+
polling: true,
|
|
1290
|
+
note: 'Token will activate automatically once approved. Just retry your previous tool call.',
|
|
1291
|
+
}),
|
|
1292
|
+
}],
|
|
1293
|
+
};
|
|
1294
|
+
},
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
// ── get_token ──────────────────────────────────────────────────────────
|
|
1298
|
+
server.tool(
|
|
1299
|
+
'get_token',
|
|
1300
|
+
'Check if the MCP session has an active auth token. Use after calling `auth` to poll whether the human has approved the request yet.',
|
|
1301
|
+
{},
|
|
1302
|
+
async () => {
|
|
1303
|
+
if (token) {
|
|
1304
|
+
return {
|
|
1305
|
+
content: [{
|
|
1306
|
+
type: 'text' as const,
|
|
1307
|
+
text: JSON.stringify({
|
|
1308
|
+
hasToken: true,
|
|
1309
|
+
agentId: pendingAuth?.agentId || 'mcp-stdio',
|
|
1310
|
+
}),
|
|
1311
|
+
}],
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (pendingAuth) {
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{
|
|
1318
|
+
type: 'text' as const,
|
|
1319
|
+
text: JSON.stringify({
|
|
1320
|
+
hasToken: false,
|
|
1321
|
+
status: pendingAuth.status,
|
|
1322
|
+
requestId: pendingAuth.requestId,
|
|
1323
|
+
agentId: pendingAuth.agentId,
|
|
1324
|
+
...(pendingAuth.status === 'polling' && { approveUrl: pendingAuth.approveUrl }),
|
|
1325
|
+
...(pendingAuth.status === 'polling' && { note: 'Still waiting for human approval. Retry in a few seconds.' }),
|
|
1326
|
+
...(pendingAuth.status === 'rejected' && { note: 'Auth request was rejected. Call `auth` to create a new request.' }),
|
|
1327
|
+
...(pendingAuth.status === 'timeout' && { note: 'Auth request timed out. Call `auth` to create a new request.' }),
|
|
1328
|
+
}),
|
|
1329
|
+
}],
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
return {
|
|
1334
|
+
content: [{
|
|
1335
|
+
type: 'text' as const,
|
|
1336
|
+
text: JSON.stringify({
|
|
1337
|
+
hasToken: false,
|
|
1338
|
+
note: 'No auth request in progress. Call `auth` to request a token.',
|
|
1339
|
+
}),
|
|
1340
|
+
}],
|
|
1341
|
+
};
|
|
1342
|
+
},
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
// ── start ──────────────────────────────────────────────────────────────
|
|
1346
|
+
server.tool(
|
|
1347
|
+
'start',
|
|
1348
|
+
'Start the AuraMaxx server if not already running. Checks health first, then starts in headless mode if needed. Attempts socket bootstrap after start.',
|
|
1349
|
+
{},
|
|
1350
|
+
async () => {
|
|
1351
|
+
const base = WALLET_BASE();
|
|
1352
|
+
|
|
1353
|
+
// Step 1: Check if server is already running
|
|
1354
|
+
try {
|
|
1355
|
+
const res = await fetch(`${base}/setup`, {
|
|
1356
|
+
signal: AbortSignal.timeout(3000),
|
|
1357
|
+
});
|
|
1358
|
+
if (res.ok) {
|
|
1359
|
+
// Already running — try socket bootstrap if no token
|
|
1360
|
+
if (!token) {
|
|
1361
|
+
const ok = await bootstrapViaSocket();
|
|
1362
|
+
if (ok) scheduleRefresh();
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
content: [{
|
|
1366
|
+
type: 'text' as const,
|
|
1367
|
+
text: JSON.stringify({
|
|
1368
|
+
success: true,
|
|
1369
|
+
message: 'Server is already running',
|
|
1370
|
+
hasToken: !!token,
|
|
1371
|
+
url: base,
|
|
1372
|
+
}),
|
|
1373
|
+
}],
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
} catch {
|
|
1377
|
+
// Server not reachable — proceed to start
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Step 2: Import and call startServer
|
|
1381
|
+
try {
|
|
1382
|
+
const { startServer } = await import('../cli/lib/process');
|
|
1383
|
+
startServer({ headless: true });
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Failed to start server: ${err}` }) }] };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Step 3: Wait for server to become reachable (up to 15s)
|
|
1389
|
+
const startedAt = Date.now();
|
|
1390
|
+
const maxWaitMs = 15_000;
|
|
1391
|
+
let serverReady = false;
|
|
1392
|
+
|
|
1393
|
+
while (Date.now() - startedAt < maxWaitMs) {
|
|
1394
|
+
try {
|
|
1395
|
+
const res = await fetch(`${base}/setup`, {
|
|
1396
|
+
signal: AbortSignal.timeout(2000),
|
|
1397
|
+
});
|
|
1398
|
+
if (res.ok) {
|
|
1399
|
+
serverReady = true;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
} catch {
|
|
1403
|
+
// Not ready yet
|
|
1404
|
+
}
|
|
1405
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (!serverReady) {
|
|
1409
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Server started but did not become reachable within 15 seconds' }) }] };
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Step 4: Attempt socket bootstrap
|
|
1413
|
+
if (!token) {
|
|
1414
|
+
const ok = await bootstrapViaSocket();
|
|
1415
|
+
if (ok) scheduleRefresh();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return {
|
|
1419
|
+
content: [{
|
|
1420
|
+
type: 'text' as const,
|
|
1421
|
+
text: JSON.stringify({
|
|
1422
|
+
success: true,
|
|
1423
|
+
message: 'Server started in headless mode',
|
|
1424
|
+
hasToken: !!token,
|
|
1425
|
+
url: base,
|
|
1426
|
+
}),
|
|
1427
|
+
}],
|
|
1428
|
+
};
|
|
1429
|
+
},
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
// ── unlock ─────────────────────────────────────────────────────────────
|
|
1433
|
+
server.tool(
|
|
1434
|
+
'unlock',
|
|
1435
|
+
'Unlock the vault with the provided password. Encrypts the password client-side using the server\'s RSA public key, then sends the encrypted payload. On success, activates an admin token for this MCP session. Optionally specify a vaultId to unlock a specific vault.',
|
|
1436
|
+
{
|
|
1437
|
+
password: z.string().describe('The vault password (plaintext — encrypted before transmission)'),
|
|
1438
|
+
vaultId: z.string().optional().describe('Optional vault ID to unlock a specific vault (default: primary)'),
|
|
1439
|
+
},
|
|
1440
|
+
async (input) => {
|
|
1441
|
+
const { password, vaultId } = input as { password: string; vaultId?: string };
|
|
1442
|
+
|
|
1443
|
+
if (!password.trim()) {
|
|
1444
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'password is required' }) }] };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const base = WALLET_BASE();
|
|
1448
|
+
|
|
1449
|
+
// Step 1: Fetch server's RSA public key
|
|
1450
|
+
let serverPubKey: string;
|
|
1451
|
+
try {
|
|
1452
|
+
const res = await fetch(`${base}/auth/connect`, {
|
|
1453
|
+
signal: AbortSignal.timeout(5000),
|
|
1454
|
+
});
|
|
1455
|
+
if (!res.ok) {
|
|
1456
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Failed to fetch server public key (${res.status})` }) }] };
|
|
1457
|
+
}
|
|
1458
|
+
const data = await res.json() as { publicKey: string };
|
|
1459
|
+
serverPubKey = data.publicKey;
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Server not reachable: ${err}` }) }] };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Step 2: Encrypt password with server's RSA public key
|
|
1465
|
+
let encrypted: string;
|
|
1466
|
+
try {
|
|
1467
|
+
const { publicEncrypt, constants: cryptoConstants } = await import('crypto');
|
|
1468
|
+
encrypted = publicEncrypt(
|
|
1469
|
+
{ key: serverPubKey, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
1470
|
+
Buffer.from(password),
|
|
1471
|
+
).toString('base64');
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Encryption failed: ${err}` }) }] };
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Step 3: POST /unlock with encrypted password + our ephemeral pubkey
|
|
1477
|
+
const endpoint = vaultId ? `/unlock/${encodeURIComponent(vaultId)}` : '/unlock';
|
|
1478
|
+
try {
|
|
1479
|
+
const res = await fetch(`${base}${endpoint}`, {
|
|
1480
|
+
method: 'POST',
|
|
1481
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1482
|
+
body: JSON.stringify({ encrypted, pubkey: ephemeralPubPem }),
|
|
1483
|
+
signal: AbortSignal.timeout(10_000),
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const text = await res.text();
|
|
1487
|
+
if (!res.ok) {
|
|
1488
|
+
let errorMsg: string;
|
|
1489
|
+
try { errorMsg = (JSON.parse(text) as { error?: string }).error || text; } catch { errorMsg = text; }
|
|
1490
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Unlock failed (${res.status}): ${errorMsg}` }) }] };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const data = JSON.parse(text) as { success: boolean; token?: string; address?: string; message?: string };
|
|
1494
|
+
|
|
1495
|
+
// Activate the returned admin token for this MCP session
|
|
1496
|
+
if (data.token) {
|
|
1497
|
+
token = data.token;
|
|
1498
|
+
scheduleRefresh();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
return {
|
|
1502
|
+
content: [{
|
|
1503
|
+
type: 'text' as const,
|
|
1504
|
+
text: JSON.stringify({
|
|
1505
|
+
success: true,
|
|
1506
|
+
message: data.message || 'Vault unlocked',
|
|
1507
|
+
address: data.address,
|
|
1508
|
+
hasToken: !!token,
|
|
1509
|
+
...(vaultId ? { vaultId } : {}),
|
|
1510
|
+
}),
|
|
1511
|
+
}],
|
|
1512
|
+
};
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Unlock request failed: ${err}` }) }] };
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
);
|
|
1518
|
+
|
|
1519
|
+
// ── doctor ─────────────────────────────────────────────────────────────
|
|
1520
|
+
server.tool(
|
|
1521
|
+
'doctor',
|
|
1522
|
+
'Run onboarding and runtime diagnostics (CLI equivalent: `auramaxx doctor --json`). Returns structured check results with pass/warn/fail status, findings, evidence, and remediation steps.',
|
|
1523
|
+
{
|
|
1524
|
+
strict: z.boolean().optional().describe('If true, treats warnings as failures (default: false)'),
|
|
1525
|
+
fix: z.boolean().optional().describe('If true, attempts auto-fixes like shell fallback installation (default: false)'),
|
|
1526
|
+
},
|
|
1527
|
+
async (input) => {
|
|
1528
|
+
const { strict, fix } = input as { strict?: boolean; fix?: boolean };
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
const { runDoctor } = await import('../cli/commands/doctor');
|
|
1532
|
+
const result = await runDoctor({ json: true, strict: !!strict, fix: !!fix });
|
|
1533
|
+
|
|
1534
|
+
return {
|
|
1535
|
+
content: [{
|
|
1536
|
+
type: 'text' as const,
|
|
1537
|
+
text: JSON.stringify(result),
|
|
1538
|
+
}],
|
|
1539
|
+
};
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Doctor failed: ${err}` }) }] };
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
// ── Register shared tools ──────────────────────────────────────────────
|
|
1547
|
+
|
|
1548
|
+
for (const tool of TOOLS) {
|
|
1549
|
+
const shape = jsonSchemaToZod(tool.parameters.properties, tool.parameters.required || []);
|
|
1550
|
+
|
|
1551
|
+
server.tool(
|
|
1552
|
+
tool.name,
|
|
1553
|
+
tool.description,
|
|
1554
|
+
shape,
|
|
1555
|
+
async (input) => {
|
|
1556
|
+
const result = await executeTool(tool.name, input as Record<string, unknown>, token);
|
|
1557
|
+
return { content: [{ type: 'text' as const, text: result }] };
|
|
1558
|
+
},
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
1563
|
+
|
|
1564
|
+
async function main() {
|
|
1565
|
+
// Bootstrap auth: try socket first, then env var
|
|
1566
|
+
if (!token) {
|
|
1567
|
+
const ok = await bootstrapViaSocket();
|
|
1568
|
+
if (ok) {
|
|
1569
|
+
scheduleRefresh();
|
|
1570
|
+
console.error('[mcp] Auth: socket bootstrap');
|
|
1571
|
+
} else if (process.env.AURA_TOKEN) {
|
|
1572
|
+
token = process.env.AURA_TOKEN;
|
|
1573
|
+
console.error('[mcp] Auth: AURA_TOKEN env var');
|
|
1574
|
+
} else {
|
|
1575
|
+
console.error('[mcp] Auth: none (tools will return auth errors until server is running)');
|
|
1576
|
+
}
|
|
1577
|
+
} else {
|
|
1578
|
+
// Have AURA_TOKEN from env — still try socket for encrypted upgrade
|
|
1579
|
+
console.error('[mcp] Auth: AURA_TOKEN env var (pre-configured)');
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const transport = new StdioServerTransport();
|
|
1583
|
+
await server.connect(transport);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
main().catch((err) => {
|
|
1587
|
+
console.error('MCP server error:', err);
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
});
|