@sage-protocol/pi-adapter 0.3.7 → 0.3.8

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.
@@ -160,6 +160,7 @@ const DOMAINLESS_ACTION_DOMAIN_HINTS = {
160
160
  };
161
161
  const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
162
162
  prompts: "list",
163
+ behaviors: "recommend",
163
164
  skills: "list",
164
165
  builder: "recommend",
165
166
  governance: "list_subdaos",
@@ -170,6 +171,8 @@ const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
170
171
  security: "scan",
171
172
  meta: "get_project_context",
172
173
  hub: "status",
174
+ activation: "active",
175
+ wallet: "balance",
173
176
  library: "get_active",
174
177
  help: "list",
175
178
  external: "list_servers",
@@ -638,6 +641,53 @@ function formatOperationSubject(request) {
638
641
  }
639
642
  return `${request.domain}/${request.action}`;
640
643
  }
644
+ function summarizeInventoryItem(value) {
645
+ if (typeof value === "string" && value.trim()) {
646
+ return truncateText(value.trim(), 96);
647
+ }
648
+ if (!value || typeof value !== "object")
649
+ return null;
650
+ const item = value;
651
+ const toolName = stringValue(item.tool_name);
652
+ const sessionId = stringValue(item.session_id);
653
+ const instanceId = stringValue(item.instance_id);
654
+ const serverId = stringValue(item.server_id) ?? stringValue(item.id) ?? instanceId;
655
+ if (toolName && serverId) {
656
+ return truncateText(`${toolName} @ ${serverId}`, 96);
657
+ }
658
+ if (instanceId) {
659
+ return truncateText(sessionId ? `${instanceId} (session ${sessionId})` : instanceId, 96);
660
+ }
661
+ const label = stringValue(item.title) ??
662
+ stringValue(item.name) ??
663
+ stringValue(item.key) ??
664
+ stringValue(item.id) ??
665
+ stringValue(item.url) ??
666
+ serverId;
667
+ if (!label)
668
+ return null;
669
+ const suffix = [];
670
+ if (typeof item.running === "boolean") {
671
+ suffix.push(item.running ? "running" : "stopped");
672
+ }
673
+ if (typeof item.enabled === "boolean") {
674
+ suffix.push(item.enabled ? "enabled" : "disabled");
675
+ }
676
+ if (typeof item.available === "boolean" && item.available === false) {
677
+ suffix.push("unavailable");
678
+ }
679
+ if (Array.isArray(item.tools)) {
680
+ suffix.push(`${item.tools.length} ${item.tools.length === 1 ? "tool" : "tools"}`);
681
+ }
682
+ else if (typeof item.tool_count === "number") {
683
+ suffix.push(`${item.tool_count} ${item.tool_count === 1 ? "tool" : "tools"}`);
684
+ }
685
+ const status = stringValue(item.status);
686
+ if (status && !suffix.includes(status))
687
+ suffix.push(status);
688
+ const decorated = suffix.length > 0 ? `${label} (${suffix.slice(0, 3).join(", ")})` : label;
689
+ return truncateText(decorated, 96);
690
+ }
641
691
  function formatOperationDetails(request) {
642
692
  const details = [];
643
693
  if (request.domain === "external") {
@@ -659,6 +709,7 @@ function formatOperationDetails(request) {
659
709
  const query = stringValue(request.params.query);
660
710
  const key = stringValue(request.params.key) ?? stringValue(request.params.name);
661
711
  const serverId = stringValue(request.params.server_id);
712
+ const sessionId = stringValue(request.params.session_id);
662
713
  const library = stringValue(request.params.library);
663
714
  if (query)
664
715
  details.push(`query: ${quotedText(query)}`);
@@ -666,6 +717,8 @@ function formatOperationDetails(request) {
666
717
  details.push(`key: ${truncateText(key, 84)}`);
667
718
  if (serverId)
668
719
  details.push(`server: ${serverId}`);
720
+ if (sessionId)
721
+ details.push(`session: ${truncateText(sessionId, 84)}`);
669
722
  if (library)
670
723
  details.push(`library: ${truncateText(library, 84)}`);
671
724
  return details;
@@ -675,6 +728,8 @@ function summarizePayloadItems(payload) {
675
728
  ["results", "results"],
676
729
  ["tools", "tools"],
677
730
  ["servers", "servers"],
731
+ ["running_servers", "servers"],
732
+ ["sessions", "sessions"],
678
733
  ["skills", "skills"],
679
734
  ["prompts", "prompts"],
680
735
  ["libraries", "libraries"],
@@ -682,26 +737,12 @@ function summarizePayloadItems(payload) {
682
737
  ["rooms", "rooms"],
683
738
  ["messages", "messages"],
684
739
  ];
685
- const getItemLabel = (value) => {
686
- if (typeof value === "string" && value.trim())
687
- return truncateText(value.trim(), 96);
688
- if (!value || typeof value !== "object")
689
- return null;
690
- const item = value;
691
- const label = stringValue(item.title) ??
692
- stringValue(item.name) ??
693
- stringValue(item.key) ??
694
- stringValue(item.id) ??
695
- stringValue(item.url) ??
696
- stringValue(item.server_id);
697
- return label ? truncateText(label, 96) : null;
698
- };
699
740
  if (Array.isArray(payload)) {
700
741
  return {
701
742
  count: payload.length,
702
743
  label: payload.length === 1 ? "item" : "items",
703
744
  items: payload
704
- .map(getItemLabel)
745
+ .map(summarizeInventoryItem)
705
746
  .filter((item) => Boolean(item))
706
747
  .slice(0, 4),
707
748
  };
@@ -717,7 +758,7 @@ function summarizePayloadItems(payload) {
717
758
  count: items.length,
718
759
  label: items.length === 1 ? label.replace(/s$/, "") : label,
719
760
  items: items
720
- .map(getItemLabel)
761
+ .map(summarizeInventoryItem)
721
762
  .filter((item) => Boolean(item))
722
763
  .slice(0, 4),
723
764
  };
@@ -755,24 +796,55 @@ function summarizeRenderedResult(request, details, isError) {
755
796
  return { status: "failed", preview };
756
797
  }
757
798
  const payload = extractJsonFromMcpResult(details) ?? details;
758
- const listSummary = summarizePayloadItems(payload);
759
- if (listSummary) {
760
- return {
761
- status: `${listSummary.count} ${listSummary.label}`,
762
- preview: listSummary.items,
763
- };
764
- }
765
799
  if (payload && typeof payload === "object") {
766
800
  const obj = payload;
801
+ const explicitStatus = stringValue(obj.status);
802
+ if (request?.domain === "hub" && request.action === "start") {
803
+ const tools = Array.isArray(obj.tools)
804
+ ? obj.tools
805
+ .map((item) => summarizeInventoryItem(item))
806
+ .filter((item) => Boolean(item))
807
+ .slice(0, 3)
808
+ : [];
809
+ const preview = [
810
+ ...tools,
811
+ ...[
812
+ stringValue(obj.hint),
813
+ stringValue(obj.message),
814
+ stringValue(obj.summary),
815
+ ]
816
+ .filter((item) => Boolean(item))
817
+ .map((item) => truncateText(item, 120)),
818
+ ].slice(0, 3);
819
+ return {
820
+ status: explicitStatus ?? defaultOperationStatus(request),
821
+ preview,
822
+ };
823
+ }
824
+ const listSummary = summarizePayloadItems(payload);
825
+ if (listSummary) {
826
+ const preview = listSummary.items.length
827
+ ? listSummary.items
828
+ : stringValue(obj.hint)
829
+ ? [truncateText(stringValue(obj.hint) ?? "", 120)]
830
+ : [];
831
+ return {
832
+ status: `${listSummary.count} ${listSummary.label}`,
833
+ preview,
834
+ };
835
+ }
767
836
  const message = stringValue(obj.message) ??
768
837
  stringValue(obj.summary) ??
769
- stringValue(obj.status);
838
+ stringValue(obj.hint);
770
839
  if (message) {
771
840
  return {
772
- status: defaultOperationStatus(request),
841
+ status: explicitStatus ?? defaultOperationStatus(request),
773
842
  preview: [truncateText(message, 120)],
774
843
  };
775
844
  }
845
+ if (explicitStatus) {
846
+ return { status: explicitStatus, preview: [] };
847
+ }
776
848
  if (obj.ok === true) {
777
849
  return { status: defaultOperationStatus(request), preview: [] };
778
850
  }
@@ -1461,6 +1533,7 @@ export class SageHookRuntime {
1461
1533
  }
1462
1534
  const SageDomainType = Type.Union([
1463
1535
  Type.Literal("prompts"),
1536
+ Type.Literal("behaviors"),
1464
1537
  Type.Literal("skills"),
1465
1538
  Type.Literal("builder"),
1466
1539
  Type.Literal("governance"),
@@ -1471,6 +1544,8 @@ const SageDomainType = Type.Union([
1471
1544
  Type.Literal("security"),
1472
1545
  Type.Literal("meta"),
1473
1546
  Type.Literal("hub"),
1547
+ Type.Literal("activation"),
1548
+ Type.Literal("wallet"),
1474
1549
  Type.Literal("library"),
1475
1550
  Type.Literal("help"),
1476
1551
  Type.Literal("external"),
@@ -1479,20 +1554,18 @@ export function createSageSearchTool(mcpBridge) {
1479
1554
  return {
1480
1555
  name: "sage_search",
1481
1556
  label: "Sage: Search",
1482
- promptSnippet: "Search the web, documentation, prompts, skills, and external services via Sage MCP bridge",
1557
+ promptSnippet: "Route read-only Sage code-mode requests: discover domains/actions, inspect context, query hub/external tools, and search capabilities",
1483
1558
  promptGuidelines: [
1484
- "Use sage_search (NOT bash sage commands) for web searches, documentation lookups, and prompt/skill discovery",
1485
- "NEVER run 'sage' CLI via bash -- always use the sage_search or sage_execute tools instead",
1486
- "For web search: sage_search({ domain: 'external', action: 'search', params: { tool_name: 'web_search', tool_params: { query: '...' } } })",
1487
- "To list available tools: sage_search({ domain: 'external', action: 'list_servers' })",
1559
+ "Use sage_search (NOT bash sage commands) for Sage read/discovery flows such as help.list, prompts.search, behaviors.show, meta.get_project_context, meta.suggest, hub.list_servers/status, wallet.balance/address, and external.search",
1560
+ "Use sage_search({ domain: 'help', action: 'list', params: {} }) before guessing unknown Sage domains or actions",
1561
+ "Use sage_search({ domain: 'hub', action: 'list_servers', params: {} }) for MCP server inventory; use domain='external', action='list_servers' only to inspect running external servers and tool exposure",
1562
+ "If Sage says an action is a mutation or non-search inspect action, retry with sage_execute instead of falling back to bash",
1488
1563
  ],
1489
- description: "Search the web, find documentation, search prompts/skills, and query external services. " +
1490
- "For web search: domain='external', action='search', params={tool_name:'web_search', tool_params:{query:'your query'}}. " +
1491
- "For prompt search: domain='prompts', action='search', params={query:'search terms'}. " +
1492
- "For skill search: domain='skills', action='search', params={query:'search terms'}. " +
1493
- "For GitHub code search: domain='external', action='search', params={tool_name:'github_search_code', tool_params:{query:'...'}}. " +
1494
- "For library docs: domain='external', action='search', params={tool_name:'get_library_docs', tool_params:{context7CompatibleLibraryID:'/lib/id'}}. " +
1495
- "To list available external servers/tools: domain='external', action='list_servers'.",
1564
+ description: "Route read-only Sage code-mode requests across prompts, behaviors, skills, builder, governance, chat, social, RLM, security, meta, hub, activation, wallet, library, help, and external services. " +
1565
+ "Start with domain='help', action='list' when you do not know the route. " +
1566
+ "Use domain='meta', action='get_project_context' for local Sage state and domain='meta', action='suggest' with params={goal:'...'} when you need route suggestions. " +
1567
+ "Use domain='hub' for MCP hub inventory/status and domain='external' for external tool discovery/search. " +
1568
+ "Some heavy inspect actions such as prompts.get and skills.get are classified by Sage as non-search and should use sage_execute.",
1496
1569
  parameters: Type.Object({
1497
1570
  domain: Type.Optional(SageDomainType),
1498
1571
  action: Type.Optional(Type.String()),
@@ -1543,16 +1616,15 @@ export function createSageExecuteTool(mcpBridge) {
1543
1616
  return {
1544
1617
  name: "sage_execute",
1545
1618
  label: "Sage: Execute",
1546
- promptSnippet: "Execute mutations: call external tools, use pi's hub-compat layer, create prompts, fetch URLs",
1619
+ promptSnippet: "Execute Sage mutations and non-search inspect actions: activate skills, fetch full prompt/skill bodies, manage hub servers, and call external tools",
1547
1620
  promptGuidelines: [
1548
- "Use sage_execute (NOT bash sage commands) for calling external tools, fetching URLs, and managing MCP servers",
1621
+ "Use sage_execute (NOT bash sage commands) for Sage mutations and for Sage actions that the code-mode router classifies as non-search, including prompts.get, skills.get/use, behaviors.run, activation.activate/deactivate, library writes, hub.start/stop, and external.call/execute",
1622
+ "After starting an MCP server with sage_execute({ domain: 'hub', action: 'start', params: { server_id: '...' } }), call its tools through domain='external' with params { tool_name, tool_params } instead of invoking sage via bash",
1549
1623
  "To fetch a URL: sage_execute({ domain: 'external', action: 'call', params: { tool_name: 'fetch', tool_params: { url: 'https://...' } } })",
1550
1624
  ],
1551
- description: "Execute mutations: create prompts, use skills, send chat messages, call external tools, and manage external MCP servers through pi's hub-compat wrapper. " +
1552
- "Preferred raw MCP clients use direct hub_* tools; in pi use domain='hub', action='start', params={server_id:'duckduckgo'} when needed. " +
1553
- "To use a skill: domain='skills', action='use', params={name:'skill-name'}. " +
1554
- "To call an external tool: domain='external', action='call', params={tool_name:'tool_name', tool_params:{...}}. " +
1555
- "To fetch a URL: domain='external', action='call', params={tool_name:'fetch', tool_params:{url:'https://example.com'}}.",
1625
+ description: "Execute Sage mutations and non-search inspect actions across prompts, behaviors, skills, builder, chat, library, activation, hub, and external tool surfaces. " +
1626
+ "Use this for prompts.get, skills.get/use, behaviors.run, activation.activate, library.prompt_add/skill_add, hub.start/stop, and external.call/execute. " +
1627
+ "Use domain='external', action='call' with params={tool_name:'fetch', tool_params:{url:'https://example.com'}} for URL fetches, and use domain='hub' to start or stop MCP servers before calling their tools.",
1556
1628
  parameters: Type.Object({
1557
1629
  domain: Type.Optional(SageDomainType),
1558
1630
  action: Type.Optional(Type.String()),
@@ -1585,34 +1657,7 @@ export function createSageExecuteTool(mcpBridge) {
1585
1657
  return toToolResultText(result);
1586
1658
  }
1587
1659
  catch (error) {
1588
- let errorMsg = error instanceof Error ? error.message : String(error);
1589
- // Detect hub start failures due to missing credentials and add helpful context
1590
- try {
1591
- const normalized = normalizeCodeModeRequest("execute", params);
1592
- if (normalized.domain === "hub" &&
1593
- normalized.action === "start" &&
1594
- /EOF|empty|parse|unexpected end/i.test(errorMsg)) {
1595
- const serverId = String(normalized.params.server_id ?? "unknown");
1596
- const knownKeyRequirements = {
1597
- "brave-search": "BRAVE_API_KEY",
1598
- github: "GITHUB_TOKEN",
1599
- };
1600
- const requiredKey = knownKeyRequirements[serverId];
1601
- if (requiredKey) {
1602
- errorMsg +=
1603
- `. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
1604
- `Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`;
1605
- }
1606
- else {
1607
- errorMsg +=
1608
- `. Server '${serverId}' may require environment variables (API keys). ` +
1609
- `Check with: sage mcp hub env list ${serverId}`;
1610
- }
1611
- }
1612
- }
1613
- catch {
1614
- // normalizeCodeModeRequest can throw -- ignore, keep original errorMsg
1615
- }
1660
+ const errorMsg = augmentHubStartErrorMessage(params, error instanceof Error ? error.message : String(error));
1616
1661
  return {
1617
1662
  content: [
1618
1663
  {
@@ -1627,21 +1672,269 @@ export function createSageExecuteTool(mcpBridge) {
1627
1672
  };
1628
1673
  }
1629
1674
  const SAGE_CLI_REFERENCE = `
1630
- ## Sage CLI Reference (on-demand)
1675
+ ## Sage Code-Mode Reference
1631
1676
 
1632
- ### Skill Discovery
1633
- - sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1634
- - sage_search({ domain: "skills", action: "get", params: { key: "..." } })
1677
+ ### Route Discovery
1678
+ - List domains/actions: sage_search({ domain: "help", action: "list", params: {} })
1679
+ - Suggest the right route: sage_search({ domain: "meta", action: "suggest", params: { goal: "..." } })
1680
+ - Inspect local Sage state: sage_search({ domain: "meta", action: "get_project_context", params: {} })
1635
1681
 
1636
- ### External Tools
1637
- - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1638
- - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1639
- - List tools: sage_search({ domain: "external", action: "list_servers" })
1682
+ ### Skills / Prompts / Behaviors
1683
+ - Search skills: sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1684
+ - Inspect skill body: sage_execute({ domain: "skills", action: "get", params: { key: "..." } })
1685
+ - Activate skill: sage_execute({ domain: "skills", action: "use", params: { key: "..." } })
1686
+ - Search prompts: sage_search({ domain: "prompts", action: "search", params: { query: "..." } })
1687
+ - Inspect prompt body: sage_execute({ domain: "prompts", action: "get", params: { key: "..." } })
1688
+ - Inspect behavior: sage_search({ domain: "behaviors", action: "show", params: { key: "..." } })
1640
1689
 
1641
- ### Hub
1642
- - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
1643
- - Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
1690
+ ### Hub / External Tools
1691
+ - List MCP servers: sage_search({ domain: "hub", action: "list_servers", params: {} })
1692
+ - Hub status: sage_search({ domain: "hub", action: "status", params: {} })
1693
+ - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "github" } })
1694
+ - Running external servers/tools: sage_search({ domain: "external", action: "list_servers", params: {} })
1695
+ - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1696
+ - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1644
1697
  `.trim();
1698
+ function buildHubRenderContext(action, params, context) {
1699
+ return {
1700
+ ...(context && typeof context === "object" ? context : {}),
1701
+ args: {
1702
+ domain: "hub",
1703
+ action,
1704
+ params,
1705
+ },
1706
+ };
1707
+ }
1708
+ function augmentHubStartErrorMessage(params, errorMsg) {
1709
+ try {
1710
+ const normalized = normalizeCodeModeRequest("execute", params);
1711
+ if (normalized.domain === "hub" &&
1712
+ normalized.action === "start" &&
1713
+ /EOF|empty|parse|unexpected end/i.test(errorMsg)) {
1714
+ const serverId = String(normalized.params.server_id ?? "unknown");
1715
+ const knownKeyRequirements = {
1716
+ "brave-search": "BRAVE_API_KEY",
1717
+ github: "GITHUB_TOKEN",
1718
+ };
1719
+ const requiredKey = knownKeyRequirements[serverId];
1720
+ if (requiredKey) {
1721
+ return (errorMsg +
1722
+ `. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
1723
+ `Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`);
1724
+ }
1725
+ return (errorMsg +
1726
+ `. Server '${serverId}' may require environment variables (API keys). ` +
1727
+ `Check with: sage mcp hub env list ${serverId}`);
1728
+ }
1729
+ }
1730
+ catch {
1731
+ // Keep original error text if normalization fails.
1732
+ }
1733
+ return errorMsg;
1734
+ }
1735
+ export function createHubListServersTool(mcpBridge) {
1736
+ return {
1737
+ name: "hub_list_servers",
1738
+ label: "Hub: List Servers",
1739
+ promptSnippet: "List Sage MCP servers directly when you need inventory/status without composing a generic sage_search call",
1740
+ promptGuidelines: [
1741
+ "Prefer hub_list_servers() for simple MCP server inventory in Pi; it is the direct convenience wrapper over domain='hub', action='list_servers'",
1742
+ "Use sage_search({ domain: 'external', action: 'list_servers', params: {} }) only when you specifically need running external servers/tools rather than the full Sage hub inventory",
1743
+ ],
1744
+ description: "List all Sage MCP servers with running/available state. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'list_servers', params:{} }).",
1745
+ parameters: Type.Object({}),
1746
+ renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "list_servers", params: {} }, theme),
1747
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("list_servers", {}, context)),
1748
+ execute: async () => {
1749
+ try {
1750
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1751
+ domain: "hub",
1752
+ action: "list_servers",
1753
+ params: {},
1754
+ });
1755
+ return toToolResultText(result);
1756
+ }
1757
+ catch (error) {
1758
+ return {
1759
+ content: [
1760
+ {
1761
+ type: "text",
1762
+ text: `hub_list_servers failed: ${error instanceof Error ? error.message : String(error)}`,
1763
+ },
1764
+ ],
1765
+ isError: true,
1766
+ };
1767
+ }
1768
+ },
1769
+ };
1770
+ }
1771
+ export function createHubStatusTool(mcpBridge) {
1772
+ return {
1773
+ name: "hub_status",
1774
+ label: "Hub: Status",
1775
+ promptSnippet: "Inspect Sage MCP hub runtime status directly when you want running server state without composing a generic sage_search call",
1776
+ promptGuidelines: [
1777
+ "Prefer hub_status() for simple MCP runtime status in Pi; it is the direct convenience wrapper over domain='hub', action='status'",
1778
+ "Use sage_status() when you need full adapter health plus Sage context, not just hub runtime state",
1779
+ ],
1780
+ description: "Inspect Sage MCP hub runtime status. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'status', params:{} }).",
1781
+ parameters: Type.Object({}),
1782
+ renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "status", params: {} }, theme),
1783
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("status", {}, context)),
1784
+ execute: async () => {
1785
+ try {
1786
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1787
+ domain: "hub",
1788
+ action: "status",
1789
+ params: {},
1790
+ });
1791
+ return toToolResultText(result);
1792
+ }
1793
+ catch (error) {
1794
+ return {
1795
+ content: [
1796
+ {
1797
+ type: "text",
1798
+ text: `hub_status failed: ${error instanceof Error ? error.message : String(error)}`,
1799
+ },
1800
+ ],
1801
+ isError: true,
1802
+ };
1803
+ }
1804
+ },
1805
+ };
1806
+ }
1807
+ export function createHubSessionListTool(mcpBridge) {
1808
+ return {
1809
+ name: "hub_session_list",
1810
+ label: "Hub: Session List",
1811
+ promptSnippet: "List active Sage MCP hub sessions directly when you need isolated instance visibility without composing a generic sage_search call",
1812
+ promptGuidelines: [
1813
+ "Prefer hub_session_list() for simple hub session inspection in Pi; it wraps domain='hub', action='session_list'",
1814
+ "Pass server_id when you want to filter sessions for one server, and use hub_stop_server with server_id plus session_id to stop a specific isolated session",
1815
+ ],
1816
+ description: "List active Sage MCP hub sessions, optionally filtered by server_id. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'session_list', params:{ server_id? } }).",
1817
+ parameters: Type.Object({
1818
+ server_id: Type.Optional(Type.String({
1819
+ description: "Optional hub server identifier to filter sessions",
1820
+ })),
1821
+ }),
1822
+ renderCall: (args, theme) => renderSageToolCall("search", {
1823
+ domain: "hub",
1824
+ action: "session_list",
1825
+ params: normalizeParams(args),
1826
+ }, theme),
1827
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("session_list", normalizeParams(context?.args), context)),
1828
+ execute: async (_toolCallId, params) => {
1829
+ try {
1830
+ const payload = normalizeParams(params);
1831
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1832
+ domain: "hub",
1833
+ action: "session_list",
1834
+ params: payload,
1835
+ });
1836
+ return toToolResultText(result);
1837
+ }
1838
+ catch (error) {
1839
+ return {
1840
+ content: [
1841
+ {
1842
+ type: "text",
1843
+ text: `hub_session_list failed: ${error instanceof Error ? error.message : String(error)}`,
1844
+ },
1845
+ ],
1846
+ isError: true,
1847
+ };
1848
+ }
1849
+ },
1850
+ };
1851
+ }
1852
+ export function createHubStopServerTool(mcpBridge) {
1853
+ return {
1854
+ name: "hub_stop_server",
1855
+ label: "Hub: Stop Server",
1856
+ promptSnippet: "Stop a Sage MCP server directly when you know the server_id and want to end its runtime cleanly",
1857
+ promptGuidelines: [
1858
+ "Prefer hub_stop_server({ server_id: '...' }) for simple MCP server shutdown in Pi; it wraps domain='hub', action='stop'",
1859
+ "Use hub_status() or hub_list_servers() after stopping a server if you need to confirm the resulting hub state",
1860
+ ],
1861
+ description: "Stop a Sage MCP server by server_id. This is a Pi-native convenience wrapper for sage_execute({ domain:'hub', action:'stop', params:{ server_id:'...' } }).",
1862
+ parameters: Type.Object({
1863
+ server_id: Type.String({
1864
+ description: "Hub server identifier to stop, such as github or brave-search",
1865
+ }),
1866
+ session_id: Type.Optional(Type.String({
1867
+ description: "Optional hub session identifier for isolated instances",
1868
+ })),
1869
+ }),
1870
+ renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "stop", params: normalizeParams(args) }, theme),
1871
+ renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("stop", normalizeParams(context?.args), context)),
1872
+ execute: async (_toolCallId, params) => {
1873
+ try {
1874
+ const payload = normalizeParams(params);
1875
+ const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
1876
+ domain: "hub",
1877
+ action: "stop",
1878
+ params: payload,
1879
+ });
1880
+ return toToolResultText(result);
1881
+ }
1882
+ catch (error) {
1883
+ return {
1884
+ content: [
1885
+ {
1886
+ type: "text",
1887
+ text: `hub_stop_server failed: ${error instanceof Error ? error.message : String(error)}`,
1888
+ },
1889
+ ],
1890
+ isError: true,
1891
+ };
1892
+ }
1893
+ },
1894
+ };
1895
+ }
1896
+ export function createHubStartServerTool(mcpBridge) {
1897
+ return {
1898
+ name: "hub_start_server",
1899
+ label: "Hub: Start Server",
1900
+ promptSnippet: "Start a Sage MCP server directly when you already know the server_id and want Pi to expose its tools",
1901
+ promptGuidelines: [
1902
+ "Prefer hub_start_server({ server_id: '...' }) for simple MCP server startup in Pi; it wraps domain='hub', action='start'",
1903
+ "After starting an external MCP server, call its tools through sage_execute/sage_search with domain='external' and params { tool_name, tool_params }",
1904
+ ],
1905
+ description: "Start a Sage MCP server by server_id. This is a Pi-native convenience wrapper for sage_execute({ domain:'hub', action:'start', params:{ server_id:'...' } }).",
1906
+ parameters: Type.Object({
1907
+ server_id: Type.String({
1908
+ description: "Hub server identifier to start, such as github or brave-search",
1909
+ }),
1910
+ session_id: Type.Optional(Type.String({
1911
+ description: "Optional hub session identifier for isolated instances",
1912
+ })),
1913
+ }),
1914
+ renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "start", params: normalizeParams(args) }, theme),
1915
+ renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("start", normalizeParams(context?.args), context)),
1916
+ execute: async (_toolCallId, params) => {
1917
+ try {
1918
+ const payload = normalizeParams(params);
1919
+ const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
1920
+ domain: "hub",
1921
+ action: "start",
1922
+ params: payload,
1923
+ });
1924
+ return toToolResultText(result);
1925
+ }
1926
+ catch (error) {
1927
+ const errorMsg = augmentHubStartErrorMessage({ domain: "hub", action: "start", params }, error instanceof Error ? error.message : String(error));
1928
+ return {
1929
+ content: [
1930
+ { type: "text", text: `hub_start_server failed: ${errorMsg}` },
1931
+ ],
1932
+ isError: true,
1933
+ };
1934
+ }
1935
+ },
1936
+ };
1937
+ }
1645
1938
  export function createSageStatusTool(mcpBridge) {
1646
1939
  return {
1647
1940
  name: "sage_status",
@@ -1791,6 +2084,11 @@ export function createSageExtensions(options) {
1791
2084
  console.error("[sage-pi-adapter] failed to start MCP bridge:", error);
1792
2085
  });
1793
2086
  const sageTools = [
2087
+ createHubListServersTool(mcpBridge),
2088
+ createHubStatusTool(mcpBridge),
2089
+ createHubSessionListTool(mcpBridge),
2090
+ createHubStartServerTool(mcpBridge),
2091
+ createHubStopServerTool(mcpBridge),
1794
2092
  createSageSearchTool(mcpBridge),
1795
2093
  createSageExecuteTool(mcpBridge),
1796
2094
  createSageStatusTool(mcpBridge),