stackone-ai 2.9.0__tar.gz → 2.10.0__tar.gz

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.
Files changed (179) hide show
  1. stackone_ai-2.10.0/.release-please-manifest.json +3 -0
  2. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/CHANGELOG.md +15 -0
  3. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/PKG-INFO +42 -8
  4. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/README.md +41 -7
  5. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/pyproject.toml +1 -1
  6. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/__init__.py +1 -1
  7. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/models.py +68 -5
  8. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/toolset.py +52 -27
  9. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_fetch_tools.py +18 -0
  10. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_tool_calling.py +212 -10
  11. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/uv.lock +1 -1
  12. stackone_ai-2.9.0/.release-please-manifest.json +0 -3
  13. stackone_ai-2.9.0/vendor/stackone-ai-node/.git +0 -1
  14. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/development-workflow.md +0 -0
  15. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/examples-standards.md +0 -0
  16. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/git-workflow.md +0 -0
  17. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/nix-workflow.md +0 -0
  18. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/no-relative-imports.md +0 -0
  19. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/package-installation.md +0 -0
  20. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/release-please-standards.md +0 -0
  21. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/uv-scripts.md +0 -0
  22. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/development-workflow.mdc +0 -0
  23. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/examples-standards.mdc +0 -0
  24. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/git-workflow.mdc +0 -0
  25. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/no-relative-imports.mdc +0 -0
  26. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/package-installation.mdc +0 -0
  27. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/release-please-standards.mdc +0 -0
  28. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/uv-scripts.mdc +0 -0
  29. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.env.example +0 -0
  30. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.envrc +0 -0
  31. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/actions/setup-nix/action.yaml +0 -0
  32. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/dependabot.yaml +0 -0
  33. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/ci.yaml +0 -0
  34. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/nix-flake-update.yaml +0 -0
  35. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/nix-flake.yaml +0 -0
  36. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/release.yaml +0 -0
  37. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitignore +0 -0
  38. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitleaks.toml +0 -0
  39. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitmodules +0 -0
  40. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.mcp.json +0 -0
  41. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.release-please-config.json +0 -0
  42. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/CLAUDE.md +0 -0
  43. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/LICENSE +0 -0
  44. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/auth_management.py +0 -0
  45. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/crewai_integration.py +0 -0
  46. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/langchain_integration.py +0 -0
  47. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/langgraph_integration.py +0 -0
  48. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/openai_integration.py +0 -0
  49. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/pydantic_ai_integration.py +0 -0
  50. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/search_tools.py +0 -0
  51. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/test_examples.py +0 -0
  52. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/flake.lock +0 -0
  53. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/flake.nix +0 -0
  54. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/justfile +0 -0
  55. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/scripts/benchmark_search.py +0 -0
  56. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/constants.py +0 -0
  57. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/feedback/__init__.py +0 -0
  58. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/feedback/tool.py +0 -0
  59. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/integrations/__init__.py +0 -0
  60. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/integrations/langgraph.py +0 -0
  61. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/local_search.py +0 -0
  62. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/py.typed +0 -0
  63. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/semantic_search.py +0 -0
  64. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/__init__.py +0 -0
  65. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/normalize.py +0 -0
  66. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/tfidf_index.py +0 -0
  67. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/conftest.py +0 -0
  68. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/mocks/serve.ts +0 -0
  69. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_agent_tools.py +0 -0
  70. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_feedback.py +0 -0
  71. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_integrations_langgraph.py +0 -0
  72. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_integrations_pydantic_ai.py +0 -0
  73. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_local_search.py +0 -0
  74. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_models.py +0 -0
  75. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_semantic_search.py +0 -0
  76. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_tfidf_index.py +0 -0
  77. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_toolset.py +0 -0
  78. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
  79. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
  80. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
  81. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
  82. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
  83. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
  84. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
  85. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
  86. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
  87. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
  88. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
  89. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
  90. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
  91. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
  92. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.envrc +0 -0
  93. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
  94. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
  95. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
  96. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
  97. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
  98. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
  99. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
  100. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
  101. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.gitignore +0 -0
  102. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
  103. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.mcp.json +0 -0
  104. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
  105. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
  106. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/AGENTS.md +0 -0
  107. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
  108. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/CLAUDE.md +0 -0
  109. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/LICENSE +0 -0
  110. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/README.md +0 -0
  111. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/README.md +0 -0
  112. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
  113. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
  114. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
  115. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
  116. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
  117. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
  118. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
  119. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
  120. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
  121. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
  122. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
  123. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
  124. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
  125. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/package.json +0 -0
  126. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
  127. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
  128. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
  129. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/flake.lock +0 -0
  130. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/flake.nix +0 -0
  131. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/knip.config.ts +0 -0
  132. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/lefthook.yaml +0 -0
  133. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
  134. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
  135. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
  136. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
  137. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
  138. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
  139. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
  140. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
  141. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/node.ts +0 -0
  142. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/package.json +0 -0
  143. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
  144. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
  145. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/consts.ts +0 -0
  146. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
  147. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/feedback.ts +0 -0
  148. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
  149. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/headers.ts +0 -0
  150. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/index.ts +0 -0
  151. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
  152. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
  153. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
  154. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
  155. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
  156. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
  157. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/schema.ts +0 -0
  158. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
  159. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
  160. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.ts +0 -0
  161. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
  162. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
  163. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
  164. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/types.ts +0 -0
  165. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
  166. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
  167. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
  168. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
  169. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
  170. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
  171. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
  172. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
  173. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
  174. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
  175. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
  176. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/tsconfig.json +0 -0
  177. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
  178. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/vitest.config.ts +0 -0
  179. {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.10.0"
3
+ }
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.10.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.9.1...stackone-ai-v2.10.0) (2026-07-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ENG-733:** support flat_prefixed MCP param style ([#192](https://github.com/StackOneHQ/stackone-ai-python/issues/192)) ([ff8fb9a](https://github.com/StackOneHQ/stackone-ai-python/commit/ff8fb9a41add55e9c31bd84e04447bb3685ab024))
9
+
10
+ ## [2.9.1](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.9.0...stackone-ai-v2.9.1) (2026-06-03)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **models:** handle binary file downloads in tool execution ([#190](https://github.com/StackOneHQ/stackone-ai-python/issues/190)) ([8983c68](https://github.com/StackOneHQ/stackone-ai-python/commit/8983c68dd2fdbdbdd52af531a9a9f1de6d72e1b8))
16
+ * **search:** make ToolsetConfigError actionable and fix misleading docs ([#188](https://github.com/StackOneHQ/stackone-ai-python/issues/188)) ([d332c27](https://github.com/StackOneHQ/stackone-ai-python/commit/d332c27d2f7b827e7ca043f60c37cb0831a92361))
17
+
3
18
  ## [2.9.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.8.0...stackone-ai-v2.9.0) (2026-04-30)
4
19
 
5
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackone-ai
3
- Version: 2.9.0
3
+ Version: 2.10.0
4
4
  Summary: agents performing actions on your SaaS
5
5
  Author-email: StackOne <support@stackone.com>
6
6
  License-File: LICENSE
@@ -149,6 +149,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
149
149
  - Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
150
150
  - Provider prefix: `["workday_*"]` matches all Workday tools
151
151
 
152
+ ## File Downloads
153
+
154
+ Actions that return a file — e.g. `googledrive_unified_download_file`, `documents_download_file`, any `*_unified_download_file` — resolve to **raw bytes plus metadata**, not parsed JSON. The SDK decides this from the response `Content-Type`: a JSON content type is parsed as before; anything else is treated as a file download. This applies to both `tool.execute()` and `tool.call()`.
155
+
156
+ ```python
157
+ tools = toolset.fetch_tools(actions=["googledrive_*"], account_ids=[account_id])
158
+ download = tools.get_tool("googledrive_unified_download_file")
159
+
160
+ result = download.execute({"id": "file-id"})
161
+
162
+ # `result` is a dict describing the file — write the bytes straight to disk:
163
+ with open(result["file_name"] or "download.bin", "wb") as f:
164
+ f.write(result["content"])
165
+ ```
166
+
167
+ The returned dict:
168
+
169
+ | Key | Type | Description |
170
+ | -------------- | ------------- | -------------------------------------------------------------------------------------------- |
171
+ | `content` | `bytes` | Raw file bytes. **Not JSON-serializable** — see the caveat below. |
172
+ | `content_type` | `str` | The file's MIME type (e.g. `application/pdf`), or `application/octet-stream` if unspecified. |
173
+ | `status_code` | `int` | HTTP status of the download response. |
174
+ | `headers` | `dict` | Response headers. |
175
+ | `file_name` | `str \| None` | Filename from the `Content-Disposition` header (handles RFC 5987 `filename*`), else `None`. |
176
+
177
+ > **Caveat:** `content` holds raw bytes, which are not JSON-serializable. If you forward tool results to an LLM — or anywhere that re-serializes them to JSON — handle or strip the `content` key (for example, base64-encode it on the LLM-facing path).
178
+
179
+ JSON responses are unchanged: any action returning `application/json` (or a `…+json` type) is parsed and returned as a dict exactly as before.
180
+
152
181
  ## Implicit Feedback (Beta)
153
182
 
154
183
  The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
@@ -263,7 +292,10 @@ For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()
263
292
  ```python
264
293
  toolset = StackOneToolSet()
265
294
  tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
266
- # or pydantic_ai(mode="search_and_execute") for agent-driven discovery
295
+
296
+ # For agent-driven discovery, enable search on the constructor:
297
+ # toolset = StackOneToolSet(search={"method": "auto"})
298
+ # tools = toolset.pydantic_ai(mode="search_and_execute")
267
299
  ```
268
300
 
269
301
  </details>
@@ -394,15 +426,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
394
426
 
395
427
  ```python
396
428
  import os
429
+ from stackone_ai import StackOneToolSet
397
430
 
398
- # Get a callable search tool
399
- toolset = StackOneToolSet()
431
+ # Get a callable search tool — search must be enabled on the toolset
432
+ toolset = StackOneToolSet(search={"method": "auto"})
400
433
  account_id = os.getenv("STACKONE_ACCOUNT_ID")
401
- all_tools = toolset.fetch_tools(account_ids=[account_id])
402
434
  search_tool = toolset.get_search_tool()
403
435
 
404
- # Search for relevant tools — returns a Tools collection
405
- tools = search_tool("manage employees", top_k=5)
436
+ # Search for relevant tools — returns a Tools collection scoped to the account
437
+ tools = search_tool("manage employees", top_k=5, account_ids=[account_id])
406
438
 
407
439
  # Execute a discovered tool directly
408
440
  tools[0](limit=10)
@@ -416,7 +448,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
416
448
  import os
417
449
  from stackone_ai import StackOneToolSet
418
450
 
419
- toolset = StackOneToolSet()
451
+ # Search must be enabled on the constructor — pass `search={}` for defaults,
452
+ # or set a backend / top_k explicitly.
453
+ toolset = StackOneToolSet(search={"method": "auto"})
420
454
 
421
455
  # Search by intent — returns Tools collection ready for any framework
422
456
  account_id = os.getenv("STACKONE_ACCOUNT_ID")
@@ -114,6 +114,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
114
114
  - Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
115
115
  - Provider prefix: `["workday_*"]` matches all Workday tools
116
116
 
117
+ ## File Downloads
118
+
119
+ Actions that return a file — e.g. `googledrive_unified_download_file`, `documents_download_file`, any `*_unified_download_file` — resolve to **raw bytes plus metadata**, not parsed JSON. The SDK decides this from the response `Content-Type`: a JSON content type is parsed as before; anything else is treated as a file download. This applies to both `tool.execute()` and `tool.call()`.
120
+
121
+ ```python
122
+ tools = toolset.fetch_tools(actions=["googledrive_*"], account_ids=[account_id])
123
+ download = tools.get_tool("googledrive_unified_download_file")
124
+
125
+ result = download.execute({"id": "file-id"})
126
+
127
+ # `result` is a dict describing the file — write the bytes straight to disk:
128
+ with open(result["file_name"] or "download.bin", "wb") as f:
129
+ f.write(result["content"])
130
+ ```
131
+
132
+ The returned dict:
133
+
134
+ | Key | Type | Description |
135
+ | -------------- | ------------- | -------------------------------------------------------------------------------------------- |
136
+ | `content` | `bytes` | Raw file bytes. **Not JSON-serializable** — see the caveat below. |
137
+ | `content_type` | `str` | The file's MIME type (e.g. `application/pdf`), or `application/octet-stream` if unspecified. |
138
+ | `status_code` | `int` | HTTP status of the download response. |
139
+ | `headers` | `dict` | Response headers. |
140
+ | `file_name` | `str \| None` | Filename from the `Content-Disposition` header (handles RFC 5987 `filename*`), else `None`. |
141
+
142
+ > **Caveat:** `content` holds raw bytes, which are not JSON-serializable. If you forward tool results to an LLM — or anywhere that re-serializes them to JSON — handle or strip the `content` key (for example, base64-encode it on the LLM-facing path).
143
+
144
+ JSON responses are unchanged: any action returning `application/json` (or a `…+json` type) is parsed and returned as a dict exactly as before.
145
+
117
146
  ## Implicit Feedback (Beta)
118
147
 
119
148
  The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
@@ -228,7 +257,10 @@ For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()
228
257
  ```python
229
258
  toolset = StackOneToolSet()
230
259
  tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
231
- # or pydantic_ai(mode="search_and_execute") for agent-driven discovery
260
+
261
+ # For agent-driven discovery, enable search on the constructor:
262
+ # toolset = StackOneToolSet(search={"method": "auto"})
263
+ # tools = toolset.pydantic_ai(mode="search_and_execute")
232
264
  ```
233
265
 
234
266
  </details>
@@ -359,15 +391,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
359
391
 
360
392
  ```python
361
393
  import os
394
+ from stackone_ai import StackOneToolSet
362
395
 
363
- # Get a callable search tool
364
- toolset = StackOneToolSet()
396
+ # Get a callable search tool — search must be enabled on the toolset
397
+ toolset = StackOneToolSet(search={"method": "auto"})
365
398
  account_id = os.getenv("STACKONE_ACCOUNT_ID")
366
- all_tools = toolset.fetch_tools(account_ids=[account_id])
367
399
  search_tool = toolset.get_search_tool()
368
400
 
369
- # Search for relevant tools — returns a Tools collection
370
- tools = search_tool("manage employees", top_k=5)
401
+ # Search for relevant tools — returns a Tools collection scoped to the account
402
+ tools = search_tool("manage employees", top_k=5, account_ids=[account_id])
371
403
 
372
404
  # Execute a discovered tool directly
373
405
  tools[0](limit=10)
@@ -381,7 +413,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
381
413
  import os
382
414
  from stackone_ai import StackOneToolSet
383
415
 
384
- toolset = StackOneToolSet()
416
+ # Search must be enabled on the constructor — pass `search={}` for defaults,
417
+ # or set a backend / top_k explicitly.
418
+ toolset = StackOneToolSet(search={"method": "auto"})
385
419
 
386
420
  # Search by intent — returns Tools collection ready for any framework
387
421
  account_id = os.getenv("STACKONE_ACCOUNT_ID")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackone-ai"
3
- version = "2.9.0"
3
+ version = "2.10.0"
4
4
  description = "agents performing actions on your SaaS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -23,4 +23,4 @@ __all__ = [
23
23
  "SemanticSearchResponse",
24
24
  "SemanticSearchError",
25
25
  ]
26
- __version__ = "2.9.0"
26
+ __version__ = "2.10.0"
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  import base64
4
4
  import json
5
5
  import logging
6
+ import re
6
7
  from collections.abc import Sequence
7
8
  from datetime import datetime, timezone
8
9
  from enum import Enum
9
10
  from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypeAlias, cast
10
- from urllib.parse import quote
11
+ from urllib.parse import quote, unquote
11
12
 
12
13
  import httpx
13
14
  from langchain_core.tools import BaseTool
@@ -57,6 +58,47 @@ def validate_method(v: str) -> str:
57
58
  return method
58
59
 
59
60
 
61
+ def _is_json_content_type(content_type: str) -> bool:
62
+ """Whether a response body should be parsed as JSON based on its Content-Type.
63
+
64
+ Only genuine JSON media types are parsed (``application/json`` and structured
65
+ suffixes such as ``application/problem+json``). Anything else - including a
66
+ missing Content-Type - is treated as opaque content (a file download), so the
67
+ raw bytes are returned instead of being force-decoded as UTF-8/JSON. This mirrors
68
+ how the StackOne generated SDKs default unknown bodies to ``application/octet-stream``.
69
+ """
70
+ media_type = content_type.split(";", 1)[0].strip().lower()
71
+ return media_type == "application/json" or media_type.endswith("+json")
72
+
73
+
74
+ def _filename_from_content_disposition(value: str | None) -> str | None:
75
+ """Extract the filename from a Content-Disposition header value, if present.
76
+
77
+ Handles both the plain ``filename="example.pdf"`` form and the RFC 5987 extended
78
+ ``filename*=UTF-8''example%20file.pdf`` form (which takes precedence when present).
79
+ The extended form is percent-decoded using its declared charset (RFC 5987 permits
80
+ both ``UTF-8`` and ``ISO-8859-1``); an unknown or empty charset falls back to UTF-8.
81
+ """
82
+ if not value:
83
+ return None
84
+ extended = re.search(r"filename\*\s*=\s*([^']*)'[^']*'([^;]+)", value, re.IGNORECASE)
85
+ if extended:
86
+ charset = extended.group(1).strip() or "utf-8"
87
+ encoded = extended.group(2).strip().strip('"')
88
+ try:
89
+ return unquote(encoded, encoding=charset, errors="replace") or None
90
+ except LookupError:
91
+ # Unrecognised charset label - decode as UTF-8 rather than failing.
92
+ return unquote(encoded, encoding="utf-8", errors="replace") or None
93
+ quoted = re.search(r'filename\s*=\s*"([^"]*)"', value, re.IGNORECASE)
94
+ if quoted:
95
+ return quoted.group(1).strip() or None
96
+ bare = re.search(r"filename\s*=\s*([^;]+)", value, re.IGNORECASE)
97
+ if bare:
98
+ return bare.group(1).strip().strip('"') or None
99
+ return None
100
+
101
+
60
102
  class ExecuteConfig(BaseModel):
61
103
  """Configuration for executing a tool against an API endpoint"""
62
104
 
@@ -206,7 +248,14 @@ class StackOneTool(BaseModel):
206
248
  options: Execution options (e.g. feedback metadata)
207
249
 
208
250
  Returns:
209
- API response as dict
251
+ For JSON responses, the parsed API response as a dict.
252
+
253
+ For file downloads (any non-JSON Content-Type, e.g. a
254
+ ``documents_download_file`` action), a dict describing the file:
255
+ ``{"content": <bytes>, "content_type": str, "status_code": int,
256
+ "headers": dict, "file_name": str | None}``. Note ``content`` holds
257
+ the raw bytes and is therefore not JSON-serializable - callers that
258
+ re-serialize tool results (e.g. for an LLM) should handle this key.
210
259
 
211
260
  Raises:
212
261
  StackOneAPIError: If the API request fails
@@ -257,9 +306,23 @@ class StackOneTool(BaseModel):
257
306
  response_status = response.status_code
258
307
  response.raise_for_status()
259
308
 
260
- result = response.json()
261
- result_payload = cast(JsonDict, result) if isinstance(result, dict) else {"result": result}
262
- return result_payload
309
+ content_type = response.headers.get("content-type", "")
310
+ if _is_json_content_type(content_type):
311
+ result = response.json()
312
+ result_payload = cast(JsonDict, result) if isinstance(result, dict) else {"result": result}
313
+ return result_payload
314
+
315
+ # Non-JSON bodies are file downloads (e.g. documents_download_file), which the
316
+ # API serves as raw binary with the file's own MIME type and a Content-Disposition
317
+ # header. Return the bytes plus metadata rather than forcing a JSON/UTF-8 decode.
318
+ # The shape mirrors the StackOne generated SDKs' download response.
319
+ return {
320
+ "content": response.content,
321
+ "content_type": content_type or "application/octet-stream",
322
+ "status_code": response.status_code,
323
+ "headers": dict(response.headers),
324
+ "file_name": _filename_from_content_disposition(response.headers.get("content-disposition")),
325
+ }
263
326
 
264
327
  except json.JSONDecodeError as exc:
265
328
  status = "error"
@@ -7,6 +7,7 @@ import fnmatch
7
7
  import json
8
8
  import logging
9
9
  import os
10
+ import re
10
11
  import threading
11
12
  from collections.abc import Coroutine, Sequence
12
13
  from dataclasses import dataclass
@@ -92,6 +93,14 @@ _RPC_PARAMETER_LOCATIONS = {
92
93
  }
93
94
  _USER_AGENT = f"stackone-ai-python/{_SDK_VERSION}"
94
95
 
96
+ # Param-style pinned on the /mcp tool-listing URL. The MCP schema and the RPC-execution unwrap
97
+ # (_split_envelope_params) must agree on this, so it is pinned rather than following the server
98
+ # default — the server default is free to change without breaking the SDK.
99
+ _MCP_PARAM_STYLE = "flat_prefixed"
100
+
101
+ # Matches a flat_prefixed envelope key: `<location>_<field>` (e.g. `path_id`, `query_limit`).
102
+ _FLAT_ENVELOPE_KEY_PATTERN = re.compile(r"^(path|query|body|headers)_(.+)$")
103
+
95
104
 
96
105
  # --- Internal tool_search + tool_execute ---
97
106
 
@@ -444,24 +453,17 @@ class _StackOneRpcTool(StackOneTool):
444
453
  ) -> dict[str, Any]:
445
454
  parsed_arguments = self._parse_arguments(arguments)
446
455
 
447
- body_payload = self._extract_record(parsed_arguments.pop("body", None))
448
- headers_payload = self._extract_record(parsed_arguments.pop("headers", None))
449
- path_payload = self._extract_record(parsed_arguments.pop("path", None))
450
- query_payload = self._extract_record(parsed_arguments.pop("query", None))
451
-
452
- rpc_body: dict[str, Any] = dict(body_payload or {})
453
- for key, value in parsed_arguments.items():
454
- rpc_body[key] = value
456
+ envelope = self._split_envelope_params(parsed_arguments)
455
457
 
456
458
  payload: dict[str, Any] = {
457
459
  "action": self.name,
458
- "body": rpc_body,
459
- "headers": self._build_action_headers(headers_payload),
460
+ "body": envelope["body"],
461
+ "headers": self._build_action_headers(envelope["headers"] or None),
460
462
  }
461
- if path_payload:
462
- payload["path"] = path_payload
463
- if query_payload:
464
- payload["query"] = query_payload
463
+ if envelope["path"]:
464
+ payload["path"] = envelope["path"]
465
+ if envelope["query"]:
466
+ payload["query"] = envelope["query"]
465
467
 
466
468
  return super().execute(payload, options=options)
467
469
 
@@ -477,10 +479,28 @@ class _StackOneRpcTool(StackOneTool):
477
479
  return dict(parsed)
478
480
 
479
481
  @staticmethod
480
- def _extract_record(value: Any) -> dict[str, Any] | None:
481
- if isinstance(value, dict):
482
- return dict(value)
483
- return None
482
+ def _split_envelope_params(params: dict[str, Any]) -> dict[str, dict[str, Any]]:
483
+ """Split LLM-supplied tool arguments into the RPC envelope (path/query/headers/body).
484
+
485
+ Tools are listed with ``?param-style=flat_prefixed``, so keys arrive as
486
+ ``<location>_<field>`` (for example ``path_id``, ``query_limit``). The prefix carries
487
+ the parameter location, so the split needs no per-action schema. A bare dict-valued
488
+ ``path``/``query``/``headers``/``body`` key is still bucketed for clients holding a
489
+ cached nested schema, and any other key falls through to the body.
490
+ """
491
+ buckets: dict[str, dict[str, Any]] = {"path": {}, "query": {}, "headers": {}, "body": {}}
492
+ for key, value in params.items():
493
+ match = _FLAT_ENVELOPE_KEY_PATTERN.match(key)
494
+ if match:
495
+ location, field = match.group(1), match.group(2)
496
+ buckets[location].setdefault(field, value)
497
+ continue
498
+ if key in ("path", "query", "headers", "body") and isinstance(value, dict):
499
+ for field, field_value in value.items():
500
+ buckets[key].setdefault(field, field_value)
501
+ continue
502
+ buckets["body"][key] = value
503
+ return buckets
484
504
 
485
505
  def _build_action_headers(self, additional_headers: dict[str, Any] | None) -> dict[str, str]:
486
506
  headers: dict[str, str] = {}
@@ -645,13 +665,14 @@ class StackOneToolSet:
645
665
 
646
666
  Example::
647
667
 
648
- toolset = StackOneToolSet()
668
+ toolset = StackOneToolSet(search={"method": "auto"})
649
669
  search_tool = toolset.get_search_tool()
650
670
  tools = search_tool("manage employee records", account_ids=["acc-123"])
651
671
  """
652
672
  if self._search_config is None:
653
673
  raise ToolsetConfigError(
654
- "Search is disabled. Initialize StackOneToolSet with a search config to enable."
674
+ "Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
675
+ "StackOneToolSet(...) to enable. See README 'Search Tool' for options."
655
676
  )
656
677
 
657
678
  config: SearchConfig = {**self._search_config}
@@ -664,7 +685,8 @@ class StackOneToolSet:
664
685
  """Build tool_search + tool_execute tools scoped to this toolset."""
665
686
  if self._search_config is None:
666
687
  raise ToolsetConfigError(
667
- "Search is disabled. Initialize StackOneToolSet with a search config to enable."
688
+ "Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
689
+ "StackOneToolSet(...) to enable. See README 'Search Tool' for options."
668
690
  )
669
691
 
670
692
  if account_ids:
@@ -713,8 +735,8 @@ class StackOneToolSet:
713
735
  toolset = StackOneToolSet()
714
736
  tools = toolset.openai()
715
737
 
716
- # Meta tools for agent-driven discovery
717
- toolset = StackOneToolSet()
738
+ # Meta tools for agent-driven discovery — search must be enabled
739
+ toolset = StackOneToolSet(search={"method": "auto"})
718
740
  tools = toolset.openai(mode="search_and_execute")
719
741
  """
720
742
  effective_account_ids = account_ids or (
@@ -783,7 +805,8 @@ class StackOneToolSet:
783
805
  tools = toolset.pydantic_ai()
784
806
  agent = Agent("openai:gpt-5.4", tools=tools)
785
807
 
786
- # Meta tools for agent-driven discovery
808
+ # Meta tools for agent-driven discovery — search must be enabled
809
+ toolset = StackOneToolSet(search={"method": "auto"})
787
810
  tools = toolset.pydantic_ai(mode="search_and_execute")
788
811
  """
789
812
  effective_account_ids = account_ids or (
@@ -929,7 +952,8 @@ class StackOneToolSet:
929
952
  """
930
953
  if self._search_config is None:
931
954
  raise ToolsetConfigError(
932
- "Search is disabled. Initialize StackOneToolSet with a search config to enable."
955
+ "Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
956
+ "StackOneToolSet(...) to enable. See README 'Search Tool' for options."
933
957
  )
934
958
 
935
959
  # Merge constructor defaults with per-call overrides
@@ -1082,7 +1106,8 @@ class StackOneToolSet:
1082
1106
  """
1083
1107
  if self._search_config is None:
1084
1108
  raise ToolsetConfigError(
1085
- "Search is disabled. Initialize StackOneToolSet with search config to enable."
1109
+ "Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
1110
+ "StackOneToolSet(...) to enable. See README 'Search Tool' for options."
1086
1111
  )
1087
1112
 
1088
1113
  # Merge constructor defaults with per-call overrides
@@ -1235,7 +1260,7 @@ class StackOneToolSet:
1235
1260
  if cached is not None:
1236
1261
  return cached
1237
1262
 
1238
- endpoint = f"{self.base_url.rstrip('/')}/mcp"
1263
+ endpoint = f"{self.base_url.rstrip('/')}/mcp?param-style={_MCP_PARAM_STYLE}"
1239
1264
 
1240
1265
  def _fetch_for_account(account: str | None) -> list[StackOneTool]:
1241
1266
  headers = self._build_mcp_headers(account)
@@ -662,3 +662,21 @@ class TestToolIndexCache:
662
662
  toolset.search_tools("bar")
663
663
 
664
664
  assert build_count["count"] == 2
665
+
666
+
667
+ class TestMcpParamStylePinning:
668
+ """The /mcp listing URL must pin param-style so the schema matches the RPC unwrap."""
669
+
670
+ def test_fetch_tools_pins_flat_prefixed_param_style(self, monkeypatch):
671
+ captured: dict[str, str] = {}
672
+
673
+ def fake_fetch(endpoint: str, headers: dict[str, str]) -> list[_McpToolDefinition]:
674
+ captured["endpoint"] = endpoint
675
+ return []
676
+
677
+ monkeypatch.setattr("stackone_ai.toolset._fetch_mcp_tools", fake_fetch)
678
+
679
+ toolset = StackOneToolSet(api_key="test-key", base_url="https://api.example.com")
680
+ toolset.fetch_tools(account_ids=["acc1"])
681
+
682
+ assert captured["endpoint"] == "https://api.example.com/mcp?param-style=flat_prefixed"