Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,34 +498,43 @@ This project uses [BWS Secure](https://github.com/last-rev-llc/bws-secure) for m

⚠️ **5.** Never commit this token to version control

### 🎯 Token Usage Options:
### 🎯 Token and project options

- **BWS_ACCESS_TOKEN**: Loads ALL projects associated with that token (recommended for multi-project setups)
- **BWS_PROJECT_ID**: Loads only a specific project (use for single-project or testing scenarios)
- **BWS_ACCESS_TOKEN**: Required to load secrets from Bitwarden (scoped to your machine account).
- **BWS_PROJECT_ID** (optional): Restrict to one or more BWS **project UUIDs**. Use a **single** UUID, or **comma-separated UUIDs** to merge projects (later IDs win when the same key exists in more than one project). Omit to use `bwsconfig.json` / project selection. Fully backward compatible with single-UUID setups.

**Example for single project:**
**Single project:**
```
BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001
```

The project ID can be found in the Bitwarden Secrets Manager, within the list of projects.
**Multiple projects (optional):**
```
BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001, 11111111-1111-1111-1111-111111111111
```

More detail: [Multi-project ID guide](https://github.com/last-rev-llc/bws-secure/blob/main/guides/MULTI_PROJECT_ID_GUIDE.md).

### Transient `.env.secure` files

Encrypted `.env.secure` / `.env.secure.*` files in the repo root are **removed when each run finishes** (after your command runs; secrets are already in the process environment). Set **`BWS_KEEP_SECURE_FILES=true`** only when you need to inspect those files.

### 🔧 Common Issues & Troubleshooting:
### 🔧 Common Issues & Troubleshooting

- **"No projects found"**: Verify your token has project access permissions in Bitwarden
- **"Access denied"**: Check that the Machine Account has read permissions for the target projects
- **"Access denied"**: Check that the Machine Account has read permissions for the target projects
- **Token not working**: Ensure no extra spaces when copying from Bitwarden
- **Multiple projects loading**: This is normal with BWS_ACCESS_TOKEN - use BWS_PROJECT_ID for single project
- **Multiple projects / overlays**: Order matters for duplicate keys—see the multi-project guide above

### Updating BWS Secure

To update BWS Secure to the latest version, you can use the convenient script that was added to your package.json:

```bash
npm run bws-update # Or use your project's package manager: yarn bws-update, pnpm bws-update
npm run bws-update # Or: yarn bws-update, pnpm bws-update
```

Alternatively, you can run the following command manually from your project root:
Alternatively, from your project root:

```bash
rm -rf scripts/bws-secure && git clone git@github.com:last-rev-llc/bws-secure.git scripts/bws-secure && rm -rf scripts/bws-secure/.git && bash scripts/bws-secure/install.sh
Expand Down
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-solid": "0.12.0",
"eslint-plugin-unused-imports": "^2.0.0",
"glob": "^11.1.0",
"glob": "^13.0.6",
"husky": "^8.0.3",
"jsdom": "^24.1.3",
"knip": "^5.83.1",
Expand All @@ -120,10 +120,19 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@prisma/client",
"@prisma/engines",
"@swc/core",
"bufferutil",
"cypress",
"esbuild",
"faiss-node",
"sqlite3"
"prisma",
"puppeteer",
"sharp",
"sqlite3",
"utf-8-validate"
],
"overrides": {
"@opentelemetry/sdk-trace-base": "1.27.0",
Expand Down
653 changes: 406 additions & 247 deletions packages-answers/ui/src/Admin/Chatflows/index.tsx

Large diffs are not rendered by default.

71 changes: 66 additions & 5 deletions packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
* - Handles token refresh automatically before MCP initialization
* - Connects to Atlassian's remote MCP server via SSE transport
* - Supports both JIRA and Confluence through single integration
* - Pre-fetches get_accessible_resources at init time and injects cloudId context
* into the description of every tool that requires it, so the LLM never has to
* guess or discover the cloudId with a separate round-trip
*
* Required environment variables:
* - ATLASSIAN_CLIENT_ID
Expand All @@ -23,6 +26,13 @@ import { MCPToolkit } from '../core'
import { getCredentialData } from '../../../../src/utils'
import { ATLASSIAN_MCP_SERVER_URL } from '../../../../src/constants'

interface AtlassianCloudResource {
id: string
url: string
name: string
scopes?: string[]
}

class Atlassian_MCP implements INode {
label: string
name: string
Expand All @@ -41,7 +51,7 @@ class Atlassian_MCP implements INode {
constructor() {
this.label = 'Atlassian MCP'
this.name = 'atlassianMcp'
this.version = 1.0
this.version = 1.1
this.type = 'Atlassian MCP Tool'
this.icon = 'atlassian.svg'
this.category = 'Tools (MCP)'
Expand Down Expand Up @@ -71,7 +81,6 @@ class Atlassian_MCP implements INode {
loadMethods = {
listActions: async (nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> => {
try {
// Check if credential exists first
if (!nodeData.credential) {
return [
{
Expand Down Expand Up @@ -108,7 +117,7 @@ class Atlassian_MCP implements INode {
const tools = await this.getTools(nodeData, options)

const _mcpActions = nodeData.inputs?.mcpActions
let mcpActions = []
let mcpActions: string[] = []
if (_mcpActions) {
try {
mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions
Expand All @@ -121,8 +130,6 @@ class Atlassian_MCP implements INode {
}

async getTools(nodeData: INodeData, options: ICommonObject): Promise<Tool[]> {
// Token refresh is handled automatically by server before node initialization
// So we can directly use the access token from credential data
const credentialData = await getCredentialData(nodeData.credential || '', options)

if (!credentialData.access_token) {
Expand All @@ -139,8 +146,62 @@ class Atlassian_MCP implements INode {

const tools = toolkit.tools ?? []

// Pre-fetch accessible cloud resources and inject cloudId context into
// the description of every tool that declares a cloudId parameter.
// This prevents the LLM from passing null/undefined and eliminates the
// need for a discovery round-trip during the actual conversation.
const getResourcesTool = tools.find((t) => t.name === 'get_accessible_resources')
if (getResourcesTool) {
const cloudResources = await this.fetchCloudResources(getResourcesTool)
if (cloudResources.length > 0) {
this.enrichToolsWithCloudContext(tools, cloudResources)
}
}

return tools
}

/**
* Calls get_accessible_resources via its existing MCP tool and returns the
* parsed list of Atlassian cloud sites available to the authenticated user.
* Returns an empty array on any error so the caller degrades gracefully.
*/
private async fetchCloudResources(getResourcesTool: Tool): Promise<AtlassianCloudResource[]> {
try {
// tool.invoke({}) returns JSON.stringify(res.content) from the MCP server,
// which is an array of content blocks, e.g.:
// [{"type":"text","text":"[{\"id\":\"...\",\"url\":\"...\",\"name\":\"...\"}]"}]
const rawResult = await getResourcesTool.invoke({})
const contentBlocks = JSON.parse(rawResult)
const textBlock = Array.isArray(contentBlocks) ? contentBlocks.find((c: any) => c.type === 'text') : null
if (!textBlock?.text) return []

const resources = JSON.parse(textBlock.text)
return Array.isArray(resources) ? resources.filter((r: any) => r.id && r.url) : []
} catch (err) {
console.warn('[Atlassian MCP] Could not pre-fetch cloud resources — cloudId context will not be injected:', err)
return []
}
}

/**
* Appends a human-readable cloudId hint to the description of every tool
* that declares a `cloudId` parameter in its zod schema. Mutates in-place.
* The LLM sees this hint in the tool description before choosing arguments.
*/
private enrichToolsWithCloudContext(tools: Tool[], resources: AtlassianCloudResource[]): void {
const cloudNote =
resources.length === 1
? ` [cloudId for this Atlassian site: "${resources[0].id}" (${resources[0].name} — ${resources[0].url})]`
: ` [Available Atlassian cloudIds: ${resources.map((r) => `"${r.id}" → ${r.name} (${r.url})`).join(', ')}]`

for (const t of tools) {
const shape = (t as any).schema?.shape
if (shape && 'cloudId' in shape) {
t.description = `${t.description}${cloudNote}`
}
}
}
}

module.exports = { nodeClass: Atlassian_MCP }
84 changes: 0 additions & 84 deletions packages/server/src/config/passport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { Strategy as OAuth2Strategy } from 'passport-oauth2'
import { Strategy as CustomStrategy } from 'passport-custom'
import { fetchMCPMetadata } from '../utils/mcp-metadata'
import { configureAuth0Strategy } from '../aai/auth/auth0Strategy'

export default function (passport: any) {
Expand Down Expand Up @@ -90,88 +88,6 @@ export default function (passport: any) {
)
}

// Atlassian MCP OAuth Strategy
// Import the OAuth utilities from utils
const { getPendingRegistration, clearPendingRegistration, createCompleteCredentialData } = require('../utils')

passport.use(
'atlassian-dynamic',
new CustomStrategy(async (req: any, done: any) => {
try {
const code = req.query?.code as string
const state = req.query?.state as string
const error = req.query?.error as string

if (error) {
return done(null, false, { message: `OAuth error: ${error}` })
}

if (!code) {
return done(null, false, { message: 'Authorization code missing' })
}

// Get MCP client info from state parameter (sessionId)
const sessionId = state
const mcpClientInfo = sessionId ? getPendingRegistration(sessionId) : null

if (!mcpClientInfo) {
return done(null, false, { message: 'MCP client info not found. Please restart the OAuth flow.' })
}

// Fetch MCP metadata and use MCP client credentials
const metadata = await fetchMCPMetadata()
const tokenURL = metadata.token_endpoint
const clientId = mcpClientInfo.client_id
const clientSecret = mcpClientInfo.client_secret

// Exchange authorization code for tokens
const tokenResponse = await fetch(tokenURL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: `${process.env.API_HOST}/api/v1/atlassian-auth/callback`
})
})

if (!tokenResponse.ok) {
const errorText = await tokenResponse.text()
console.error('Token exchange failed:', errorText)
return done(null, false, { message: `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}` })
}

const tokenData = await tokenResponse.json()

// Use the centralized function to create complete credential data
const tokens = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_in: tokenData.expires_in || 3600
}

const baseCredentialData = {
userInfo: {} // We'll skip profile fetching for now
}

const newCredential = createCompleteCredentialData(sessionId, tokens, baseCredentialData)

// Clean up the temporary session
clearPendingRegistration(sessionId)

done(null, newCredential)
} catch (error) {
console.error('Atlassian OAuth Error:', error)
done(error, undefined)
}
})
)

passport.serializeUser((user: any, done: any) => {
done(null, user)
})
Expand Down
Loading
Loading