Implement MCP OAuth 2.0/2.1 authentication with Dynamic Client Registration (fixes 401 on protected servers like Notion) - #2
Conversation
|
@copilot for notion there is not real oauth configuration, just the /mcp endpoint. how would that be handled here? |
Great point. Notion has two modes: Option A — Internal integration token (most common for personal use): you get a static token from https://www.notion.so/profile/integrations and pass it directly as a header. No "notion": {
"transport": "streamable-http",
"url": "https://mcp.notion.com/mcp",
"headers": {
"Authorization": "******"
}
}Option B — Public OAuth app: only needed when you've registered a public Notion OAuth application (has a Updated |
|
@copilot no there is also no token. could it be some dynamic client registration thing? |
Yes, exactly — Notion's remote MCP server uses OAuth 2.1 with Dynamic Client Registration (RFC 7591 / RFC 8414). There's no Implemented in commit
The minimal config for Notion is now just: "notion": {
"transport": "streamable-http",
"url": "https://mcp.notion.com/mcp",
"auth": { "type": "oauth2" }
}Then run |
ecbc7d4 to
6ce853f
Compare
…oE models Quantizing Qwen3.5-397B-A17B (and similar large MoE checkpoints) on Apple Silicon failed in two ways: 1. mx.load and mx.quantize each issue a single Metal dispatch per tensor, which exceeds the command-buffer timeout on 512x2048x4096 expert tensors. 2. mlx-vlm's Model.sanitize() returns a transformed dict containing every weight, which OOMs a 512 GB Mac on a 397B-parameter model. Changes: * Replace eager mx.load with _LazyTensorIndex, a memory-mapped view over safetensors files. Tensors are read on demand via _LazyTensor._load_rows, which sub-chunks numpy->MLX conversion to stay under both the device's max_buffer_length (queried via mx.device_info) and MLX's int32 element count limit. Chunk budgets scale with hardware: ~14 GiB per chunk on M3 Ultra, ~875 MiB on M1, with safe fallbacks if Metal info is unavailable. * Add _quantize_chunked, a drop-in replacement for mx.quantize that bisects on dim 0 and concatenates the per-chunk results. Same buffer and element-count budgets. mx.synchronize + mx.clear_cache between chunks drains the command queue. * Add _StreamingPlan, a streaming sanitizer for VLM models that builds a per-output-tensor transformation plan from the lazy index without materializing any weights. Implements the Qwen3.5 MoE Model.sanitize logic (drop mtp.*, optional lm_head tied-embedding drop, fused gate_up_proj split on axis -2, model.language_model -> language_model.model and model.visual -> vision_tower renames, lm_head -> language_model.lm_head, conv1d.weight axis (2,1) permute, +1.0 on 1D norm weights, and patch_embed Conv3d (out,in,T,H,W) -> (out,T,H,W,in) permute). Quantize loop pulls one tensor at a time via pop(), peak RAM stays bounded. Tested end-to-end on M3 Ultra 512GB with Qwen3.5-397B-A17B oQ4: model loads directly in mlx-vlm with no post-hoc converter, generates coherent output at ~30 tok/s, peak memory 229 GB. LLM-only models still go through the original _build_model_sanitizer path; only VLM checkpoints (architectures containing 'ForConditionalGeneration') use the streaming plan.
Replace JinaForRanking score-token logit scoring with the upstream listwise hidden-state projector pipeline so multilingual reranking behavior matches the model contract. Also classify JinaForRanking as a directly supported reranker architecture to avoid false negatives from CausalLM directory-name heuristics.
Replaces Qwen3.5-specific _StreamingPlan with a generic discovery mechanism
that runs the real Model.sanitize() on _TrackedTensor proxies. The proxies
record shape/dtype/lineage without materializing GPU data, and a set of
monkey-patched mx ops (stack/concatenate/split/moveaxis/transpose) capture
the transforms. Result is a plan of output_key -> {sources, transform, shape}
that _DiscoveredPlan materializes one tensor at a time with chunked stacking.
Addresses review feedback on jundot#737:
- _StreamingPlan no longer corrupts non-Qwen VLMs — it's not even in the
activation path anymore. Discovery handles every model mlx-lm/mlx-vlm
supports (tested on Gemma 4 E2B, Trinity Nano AfMoE, Qwen 3.5 397B MoE).
- _LazyTensorIndex.pop() now materializes to mx.array instead of returning
_LazyTensor, so third-party sanitizers that call mx.stack on popped
tensors work correctly.
- _LazyTensorIndex.__iter__ and items() now include _overrides keys so
sanitize-written tensors are visible during iteration.
- _LazyTensor.__getitem__ and _materialize_source now handle 0-dim scalars
(needed for Gemma 4's scaling factors).
Tested end-to-end:
- Gemma 4 E2B oQ8: generates coherent text
- Qwen 3.5 397B: unchanged behavior (discovery produces same plan
_StreamingPlan did)
ed039a1 to
dcc60f9
Compare
…port Replaces Qwen3.5-specific _StreamingPlan activation with a generic discovery mechanism that works for any model architecture: Discovery-based streaming sanitizer: - _TrackedTensor: fake tensor proxy that records shape/dtype/lineage during a sanitize() dry run. Supports reshape, astype, arithmetic, None-broadcasting indexing, and slice patterns. - _discover_sanitize_plan(): runs the real Model.sanitize() on tracked tensors with monkey-patched mx ops (stack/concatenate/split/moveaxis/ transpose/from_fp8/pad/eval/clear_cache). Produces a transform plan without materializing any GPU data. Cost: <1s even on 42K-tensor models. - _DiscoveredPlan: dict-like wrapper that materializes one tensor at a time using the discovered plan, with chunked stacking (16 experts per chunk) to bound peak memory on large MoE models. - Graceful fallback to eager sanitize if discovery fails. FP8 source model support (MiniMax-M2.7, DeepSeek FP8, etc.): - _LazyTensor: F8_E4M3 and F8_E5M2 dtype support — loaded as uint8 so sanitize can call mx.from_fp8() on them. - _streaming_fp8_dequant(): processes FP8 weight/scale_inv pairs one at a time, runs block-scaled dequant (from_fp8 + pad + reshape + scale multiply + slice), writes bf16 results to scratch safetensors shards on disk, and re-indexes the lazy loader. Peak RAM bounded to one tensor at a time regardless of model size. - FP8 sources bypass discovery (dequant chain is too complex to replay) and use eager sanitize after streaming dequant completes. Other fixes: - _LazyTensorIndex.pop() materializes mx.array instead of returning raw _LazyTensor objects. - _LazyTensorIndex.__iter__ and items() include _overrides keys. - _LazyTensor.__getitem__ and _materialize_source handle 0-dim scalars (needed for Gemma 4 scaling factors). Tested end-to-end on M3 Ultra 512GB: - Gemma 4 E2B oQ2-8: coherent output at all levels - Trinity Nano Preview (AfMoE) oQ4-8: coherent output - Qwen 3.5 397B oQ2-8: unchanged behavior - MiniMax-M2.7 (FP8 source): streaming dequant completes, oQ8 builds
The Gemma 4 chat template checks for tool_responses on the current message (the assistant turn that issued tool_calls) BEFORE falling back to a forward-scan for role='tool' messages. The previous code created a separate assistant message for tool_responses, which caused both template paths to miss — producing a corrupt bare <|tool_response> tag and making the model loop on the same tool call indefinitely. Attach tool_responses directly to the assistant message that already has tool_calls (via the existing out_msg reference). This is the companion fix to jundot#789 which preserved tool fields through the VLM engine; this fix ensures the message extractor produces the correct structure before those fields reach the template. Tested with Gemma 4 31B IT on multi-turn agentic conversations with tool use — model now sees tool results and stops looping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ting (jundot#796) get_message_json() converts string content to list format for VLM model types, which breaks simplified chat templates that only handle strings. Collapse text-only list content back to plain string after formatting.
…sponses-same-message fix: attach tool_responses to same assistant message as tool_calls
LSUIElement=true in Info.plist conflicts with the runtime Regular->Accessory activation policy switch, causing macOS ControlCenter to move the NSStatusItem to a blocked list. - remove LSUIElement from Info.plist (dock hiding already handled by setActivationPolicy_ at runtime) - add autosaveName so ControlCenter persists visibility prefs and existing blocked users get a fresh item identity - add delayed isVisible check to guide users to System Settings if ControlCenter still blocks the icon closes jundot#725, closes jundot#806
… CLI v0.3.6 relied on NSStatusItem.isVisible() and button-window frame, both of which stay "visible" on Tahoe even when ControlCenter or the Menu Bar toggle hides the icon. Switch to NSWindow.isVisible plus the NSWindowOcclusionStateVisible bit, which actually flip, and log a "menubar visibility probe" line with all raw signals for diagnostics. - Alert now activates the app, raises itself to NSFloatingWindowLevel so it surfaces from an Accessory process, and adds a "View Log" button. - Tahoe+ deep-links to System Settings > Menu Bar. Sequoia and older skip the Settings button (no system UI exists for third-party status items pre-Tahoe) and suggest restarting oMLX / checking Bartender/Ice. - Swap showAbout_ to orderFrontStandardAboutPanelWithOptions_ with a clickable GitHub link via NSLinkAttributeName so About matches other Mac apps' centered layout. - Route menubar logs to ~/Library/Application Support/oMLX/logs/menubar.log via RotatingFileHandler (the process had no file handler before). - "omlx diagnose menubar" tails both menubar.log and server.log and surfaces visibility probe lines plus manual recovery steps. Apple's sandbox blocks programmatic re-enable on Tahoe (visibility prefs live in group.com.apple.controlcenter's Group Container, unreachable by third parties; legacy plist writes are ignored on-device), so the focus here is accurate detection and clear recovery guidance. Refs jundot#725 jundot#806
d6c4b9d to
633c9ff
Compare
…polish Keeps iterating on the Tahoe 26.x menubar visibility story from jundot#725 / jundot#806. The dev3/dev4/dev5 work here is one logical change: give the "Menubar Icon Hidden" alert a user-fixable path for the two distinct failure modes users have actually reported. Detection (packaging/omlx_app/app.py): - Extract NSStatusItem creation into _create_status_item() with stable accessibility attributes (identifier / title / label / tooltip) so AX enumerators can find us. - Add a session-limited _recreate_status_item() recovery: if the 3s post-launch probe reports hidden, remove the status item and recreate it once before alerting, then reprobe after 1s. Covers the Tahoe registration race reported in Maccy jundot#1224 and Stats #2734. - Defer the Regular to Accessory policy switch to the next runloop tick via a one-shot NSTimer and dedicated switchToAccessoryPolicy_ selector, so the status item registers while the process is still Regular. - Drop the mid-session visibility probe from healthCheck_; fullscreen video, slideshows and the like triggered false-positive hidden alerts. The launch-time probe (+ one-shot recreate) is the only check now. - Probe log emits only when hidden is detected, with pid / AX signals / frame / recreated flag. No more verbose INFO spam on every tick. StatusKit Auto-Fix (packaging/omlx_app/app.py): - When the alert fires on Tahoe, user can click Auto-Fix to flip com.omlx.app's isAllowed to True in ~/Library/Group Containers/group.com.apple.controlcenter/Library/ Preferences/group.com.apple.controlcenter.plist and restart ControlCenter. Mechanism cross-referenced against anthropics/claude-code#42019 for Claude for Desktop. - Full Disk Access gate with a dedicated second dialog that deep-links to System Settings > Privacy & Security > Full Disk Access when FDA hasn't been granted yet. - Atomic write via tmp + os.replace, plist validation re-read after write, automatic restore from backup (~/Library/Application Support/oMLX/backups/statuskit-<ts>.plist) if the fresh file can't be parsed. killall ControlCenter at the end to reload. Bartender conflict (packaging/omlx_app/app.py): - Detect Bartender via NSWorkspace.runningApplications() with a com.surteesstudios.Bartender prefix match (covers 4 / 5 / future). - If Bartender is active when the alert would fire, swap in _show_bartender_conflict_alert() with Bartender-specific messaging (no Auto-Fix / no Open Settings; neither helps) and a pointer to Ice (https://icemenubar.app) as an alternative menubar manager. About panel (packaging/omlx_app/app.py): - Swap the NSAlert-based showAbout_ for orderFrontStandardAboutPanelWithOptions_ so the layout matches the standard Mac About dialog, with the GitHub URL embedded as a clickable NSLinkAttributeName in the Credits string. Info.plist (packaging/build.py): - Add NSPrincipalClass = NSApplication, which Xcode-generated bundles include by default but our manual bundle was missing. - Rebuild NSHumanReadableCopyright to use the current build year automatically, drop the version suffix (the About panel shows it separately), and add the Apache 2.0 license notice on its own line. Refs jundot#725 jundot#806 jundot#821
…#837) Adds 4 new intelligence benchmarks (BBQ, MathQA, MMLU-Pro, SafetyBench) with bundled JSONL data, plus UI grouping in the accuracy benchmark dashboard (Knowledge / Commonsense & Reasoning / Math / Coding / Safety & Alignment). QuestionResult.category now flows from scheduler through SSE payload into CSV and TXT downloads for per-subject drill-down. Co-authored-by: michal-stengg <153718997+michal-stengg@users.noreply.github.com>
…ents (jundot#814) * feat: add preserve_thinking support for Qwen 3.6+ models Qwen 3.6 chat templates strip <think> blocks from historical assistant turns by default, which breaks KV prefix cache reuse and prevents the model from seeing its own prior reasoning. Add preserve_thinking=True as an auto-detected default when the template supports it. - Auto-detect preserve_thinking support from chat template - Auto-set preserve_thinking=True when thinking is active - Add preserve_thinking to ModelSettings for manual override - Pipe through engine_pool, admin routes, and all server endpoints * fix: reconstruct <think> blocks from reasoning_content for external clients preserve_thinking only works if <think> blocks are present in historical assistant turns. External OpenAI/Anthropic-compatible clients receive clean content (thinking stripped into reasoning_content by the reasoning parser) and echo back only content on the next turn — so the template has nothing to preserve. Rebuild <think> blocks server-side from client-provided thinking before the chat template is applied: - OpenAI path: add reasoning_content to the Message request model; extract_text_content and extract_multimodal_content merge reasoning into <think>...</think>\n\n{content} for assistant turns. - Anthropic path: stop dropping "thinking" content blocks in all three convert_anthropic_to_internal sites (including the native-tool-calling assistant branch that previously had no handling at all). Append reconstructed <think> blocks in source order instead of insert-at-0 so multiple thinking blocks keep their original ordering. - Harmony branch left unchanged (gpt-oss uses <|channel|>analysis, not <think>). Tested end-to-end with Qwen3.6-35B-A3B-8bit on both endpoints: model correctly recalls the exact second 20-digit number generated in prior thinking when reasoning is echoed; fabricates a different number otherwise, confirming the fix is load-bearing. Assisted by Claude.
…es (jundot#853) * feat(model-settings): add per-model profile and global template data layer - Add Profile dataclass with settings snapshot, active flag, timestamps - Add Template dataclass with display_name and settings snapshot - Add ModelProfile dataclass linking profiles to specific model IDs - Add ModelSettingsManager.save_profile, update_profile, delete_profile, apply_profile - Add list_profiles per-model and list_templates globally - Persist profiles to model_profiles.json, templates to model_templates.json - Atomic JSON writes with temp file + rename - Cascade delete when model is removed - Add profile name validator (alphanumeric, dash, underscore; 1-64 chars) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(admin): add profile and template HTTP API endpoints - Add CreateProfileRequest, UpdateProfileRequest, CreateTemplateRequest, UpdateTemplateRequest, ModelSettingsRequest Pydantic models - Add /profiles/ POST/GET (list global templates) - Add /profiles/{name} GET/PUT/DELETE - Add /profiles/{name}/apply POST to activate a profile - Add /models/{model}/profiles/ GET list per-model profiles - Add /models/{model}/profiles/{name}/apply POST - Wire into existing admin router Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(i18n): add profile/template UI strings for 5 locales Add all user-facing i18n strings for: - Profile/template section labels and pills - Inline create/edit form placeholders and buttons - Delete confirmation prompts and action labels - Custom label for models without an active profile Locales: en, zh, zh-TW, ja, ko * feat(ui): add profile/template pills to model settings modal - Add profiles/templates section with pills in model settings modal - Replace per-parameter colored chips with single profile chip in model list - Support inline create/edit forms for both profiles and templates - Delete confirmation flow with explicit confirm step - Show active profile badge with emerald styling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore dashboard.js with profile/template frontend logic The feature branch was reset to main but dashboard.js was accidentally left with the stripped-down version. Restore the full implementation: - Alpine state: profiles[], templates[], activeProfileName, profilesDrift, CRUD flags - applyProfileToForm(), applyTemplateToForm(), computeDrift() - formValuesForProfile(), formValuesForTemplate() helpers - DELETE /profiles/{name}/ PUT /profiles/{name} calls in save/delete handlers * fix: add emerald CSS classes to Tailwind safelist for profile pills Profile pills use emerald color scheme (bg-emerald-50, text-emerald-700, border-emerald-200). JIT mode purges these unless explicitly listed in the safelist. Also update compiled CSS to include the new classes. * fix: add missing model_profiles.py module The model_profiles.py file containing Profile/Template dataclasses, field allowlists, and the validate_profile_name helper was not included in the cherry-pick. Re-create it from the original commits (4f52f46, 97727fd). * fix: add active_profile_name field to ModelSettings dataclass The field was missing from the dataclass definition, causing it to be dropped when applying a profile. The apply_profile manager method was setting merged["active_profile_name"] but ModelSettings.from_dict() was stripping it because the field didn't exist on the dataclass. * feat: eliminate field allowlist triplication - Add GET /admin/api/profile-fields endpoint serving UNIVERSAL_PROFILE_FIELDS and MODEL_SPECIFIC_PROFILE_FIELDS from model_profiles.py as single source of truth - dashboard.js fetches field lists on init (loadProfileFields) - Remove hardcoded UNIVERSAL_PROFILE_FIELDS / MODEL_SPECIFIC_PROFILE_FIELDS - applyProfileToForm and formValuesForTemplate now use profileFields.universal / profileFields.model_specific - Fix Pydantic mutable default: use Field(default_factory=dict) on CreateProfileRequest.settings and CreateTemplateRequest.settings - Fix applyProfileToForm: r.json() was a Promise (not await'd); UI state now only updated after r.ok; 401 redirect handled - model_settings.py docstring updated with all fields - Add test for /admin/api/profile-fields endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(profile): 4 high-priority robustness and drift issues 1. formValuesForProfile driven by allowlist (dashboard.js) Iterate profileFields.universal + model_specific instead of hand-writing a 50-line object. Adding a field to model_profiles.py now automatically covers it in the form serializer without a JS change. 2. Write-then-mutate rollback (model_settings.py) _save_profiles now serializes before writing; if write fails, in-memory state is unchanged. Also clean up temp file on error. 3. Rename-cascade save order (model_settings.py) _save_profiles() now runs BEFORE _save() so that if profiles write throws, settings update is not persisted (operation is atomic). Previously _save() ran first, leaving model_settings.json pointing to the new name while model_profiles.json still had the old name. 4. Apply-race guard (dashboard.js) Increment _applySeq before fetch; discard response if seq has advanced by the time it resolves. Prevents activeProfileName flipping to an earlier pill's name when clicks race. * test(profiles): catch stale allowlist entries for removed fields Guard test now also fails when a ModelSettings field is removed without cleaning its entries out of UNIVERSAL_PROFILE_FIELDS, MODEL_SPECIFIC_PROFILE_FIELDS, or EXCLUDED_FROM_PROFILES. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Xingcheng Sun <xingchengsun@Wheres-my-Laptop.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Change step from 5 to 1 for Memory Limit (Models Only) and Cold Cache Limit (SSD Cache) sliders so users can pick any integer percent, matching the Hot Cache slider. Storage format (integer GB string) is unchanged.
mlx-lm#1171 changed the MiniMax M2 tool parser to return a list when a single <minimax:tool_call> block contains multiple <invoke>s. Without list/dict flattening in api/tool_calling.py, parallel tool calls were silently dropped via AttributeError swallowed by the existing except. Flatten parser results in the native path using the same isinstance(list) pattern already used in the Gemma 4 fallback. Add regression tests for single-dict, multi-list, and multi-block-multi-invoke cases. Also picks up BatchKVCache/BatchRotatingKVCache.extend() batch-dim fix (jundot#1141) and the tree_reduce import fix (jundot#1165) from the same bump.
Agent-Logs-Url: https://github.com/convidera/omlx/sessions/16fe44b2-7940-4be4-8cc4-0fee521779b1 Co-authored-by: ChristianPraiss <6369555+ChristianPraiss@users.noreply.github.com>
…flow Agent-Logs-Url: https://github.com/convidera/omlx/sessions/e7f02143-b251-4cf7-bdfb-429b7f97e6f2 Co-authored-by: ChristianPraiss <6369555+ChristianPraiss@users.noreply.github.com>
…e Notion Agent-Logs-Url: https://github.com/convidera/omlx/sessions/5ffcc387-cbcc-485b-96ca-81e467724689 Co-authored-by: ChristianPraiss <6369555+ChristianPraiss@users.noreply.github.com>
When a request includes `execute_mcp_tools: true` (sent by the built-in
chat UI), the server now executes MCP tool calls in a loop and streams
the final model response — the client never sees intermediate tool_calls.
All other clients (API scripts, OpenClaw, etc.) get the standard OpenAI
behaviour: tool_calls are returned for the client to handle.
Also fixes two bugs in the tool message construction:
- Use "" instead of None for assistant content to avoid Jinja2 TypeError
- Guard json.loads() against returning None ("null") which broke the
Harmony chat template's `in` containment checks
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
`omlx mcp login/logout/status` now respect the configured MCP config path from settings when --mcp-config is not explicitly provided. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Emit tool_call_event chunks from server during MCP execution loops, containing tool name, arguments, and result - Add tooluse-container CSS and renderToolUseBlock() JS for collapsible tool call display (matching the thinking block style) - Rewrite renderMarkdown/renderStreamingMarkdown to process <think> and <tooluse> tags in document order via unified _renderSequential() - Replace full-innerHTML streaming updates with incremental DOM appends: completed blocks become permanent .streaming-stable nodes, only the active tail is updated in-place, preventing flicker between tool calls Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- New /admin/api/mcp/* routes: list servers with status/auth info, reconnect, authenticate (OAuth PKCE), logout, and get/save config - OAuth PKCE flow runs entirely in-browser: server returns auth_url, frontend opens a popup, callback page posts message on completion - New MCP tab in admin navbar and _mcp.html template showing server cards with transport/state/auth badges, action buttons, and a collapsible raw config JSON editor - DASHBOARD_MAIN_TABS updated to include 'mcp'; tab loads servers automatically on activation Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…erError - Fix Lucide icon processor in base.html to skip Alpine.js attributes (@, x- prefixes) when copying attributes to replacement SVG elements, preventing InvalidCharacterError: Invalid qualified name: '@mouseenter' - Replace all :class/:show bindings on <i data-lucide> elements with inline SVG spinners on wrappers — Lucide's setInterval poller replaces <i> elements and severs Alpine reactivity, making spinners and disabled states invisible; now all loading states use text changes + inline SVGs - Add tools list to GET /admin/api/mcp/servers response (name, description, param_count per tool from connected clients) - Show tools per server in an expandable section (click tool count badge) - Add mcpExpandedServer state to track which server is expanded Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
After auth completes, the server-side reconnect is an asyncio.create_task so the server is still 'connecting' when the first loadMcpServers() fires. Now poll every 1.5s (up to 12 attempts / 18s) until the server leaves the 'connecting' state, updating the message to 'Connected successfully' or 'Authenticated' accordingly. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- POST /admin/api/mcp/reload — stops current MCP manager, loads config from disk, and restarts with new config (no server restart needed) - 'Reload Config' button in the MCP tab header with spinner feedback - Banner message shows reload result (server/tool count on success, error detail on failure), auto-dismisses after 5s Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Each transport (stdio, sse, streamable-http) is now run inside its own asyncio.Task via _run_transport_in_task / _start_transport_task. The context manager's __aenter__ and __aexit__ are called within that single Task, satisfying anyio's invariant that cancel scopes must be exited from the same task that entered them. Disconnect is signalled via asyncio.Event; _cleanup_resources sets the event and awaits the task (5 s timeout then cancels) instead of calling __aexit__ directly from the caller's task. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Tool cards show 3-line clamp with pointer cursor and hover highlight - Clicking a card opens a modal overlay with the full markdown description - Modal dismisses on backdrop click, X button, or Escape key Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ind class max-h-[80vh] is not in the precompiled Tailwind CSS, so maxHeight resolved to 'none' and the modal expanded to full content height. Inline style max-height: 80vh is always applied regardless of the CSS build. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Persist discovered token_url in TokenData so token refresh works for servers using Dynamic Client Registration (fixes empty URL error) - Reuse existing registered_client_id in OAuth start flow to prevent redundant DCR on repeated authentication attempts - Only unpack tool result JSON content back to dict (not list/scalar) for gpt_oss, preventing Jinja2 |items filter failure on non-mappings Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ckaging fixes - Add omlx/mcp/builtins.py with BuiltinToolProvider exposing builtin__fetch and builtin__web_search (DuckDuckGo) — available without any MCP server - Integrate builtins into MCPClientManager (get_all_tools, execute, _find_server) - Extract inject_tool_calling() into omlx/utils/tokenizer so both VLM and batched engines share the same logic; patch gemma4 regex to match hyphenated tool names; handle mlx-lm private-attr API vs plain instance attrs - Switch packaging/build.py from pipx run to uvx for venvstacks commands - Add ddgs and truststore dependencies to pyproject.toml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
633c9ff to
a5587d6
Compare
OAuth-protected MCP servers (e.g. Notion MCP at
https://mcp.notion.com/mcp) return401 Unauthorized, causing zero tools to be discovered. The client had no OAuth lifecycle: no token acquisition, no refresh, no retry. Notion's remote MCP endpoint uses OAuth 2.1 with Dynamic Client Registration (DCR) — noclient_idneeds to be pre-configured.New modules
omlx/mcp/token_store.py—TokenData+TokenStore: persists tokens per server name using the OS keychain (keyring) with a~/.config/omlx/mcp_tokens.json(mode0o600) fallback.is_expiredincludes a 30s safety margin.TokenDatanow also storesregistered_client_idfor DCR-based servers.omlx/mcp/oauth.py—MCPOAuthManagerorchestrating two flows:flow="pkce") — opens browser, captures redirect on an ephemeral local HTTP serverflow="device") — prints user code, polls token endpointclient_idis configured, the manager auto-discovers the OAuth Authorization Server metadata (/.well-known/oauth-authorization-server) and registers the client on the fly to obtain aclient_id. The dynamically registeredclient_idis persisted inTokenDataand reused for token refresh.get_token_info()returns metadata only (raw tokens never exposed)Modified files
omlx/mcp/types.py— newMCPAuthConfigdataclass (type,client_id,auth_url,token_url,scopes,audience,device_auth_url,token_store);client_id,auth_url, andtoken_urlare optional — when omitted, DCR auto-discovery is used.MCPServerConfiggains optionalauthfield;MCPServerStatusgainsauth_state.omlx/mcp/config.py— parses theauthdict block intoMCPAuthConfig(backward-compatible; servers withoutauthare unaffected). Keys prefixed with_(e.g._comment) are stripped before parsing so documentation annotations in JSON configs don't cause errors.omlx/mcp/client.py—connect()injectsAuthorization: Bearerheader before connecting, and retries once on any 401 with a force-refreshed token._connect_sse()now passes headers to the MCP SDK'ssse_client.get_status()reportsauth_state. Server URL is forwarded to the OAuth manager to enable DCR discovery.omlx/cli.py— newomlx mcpcommand:Notion configuration
Notion's remote MCP server uses OAuth 2.1 with DCR — no developer portal registration or manual
client_idis required. The minimal config is:Then run
omlx mcp login notion. The client discovershttps://mcp.notion.com/.well-known/oauth-authorization-server, registers itself, opens a browser for consent, and stores the resulting tokens. For developers with a registered public OAuth application, explicitclient_id/auth_url/token_urlfields are still supported.