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
67 changes: 67 additions & 0 deletions .github/workflows/smart-staging-reset.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Smart Staging Reset
# Automatically resets staging to match production after staging→production releases
# Prevents 90+ file change issues in future PRs by keeping staging clean

permissions:
contents: write # Needed to push to staging branch
pull-requests: read # Needed to read PR information

on:
pull_request:
types: [closed]
branches: [production]

jobs:
reset-staging:
if: |
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref == 'staging'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Check if staging has new commits since production
id: check-staging
run: |
git fetch origin

# Get commits that are in staging but not in production
COMMITS_AHEAD=$(git rev-list --count origin/production..origin/staging)
echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT

# Get the latest staging commit hash
LATEST_STAGING=$(git rev-parse origin/staging)
echo "latest_staging=$LATEST_STAGING" >> $GITHUB_OUTPUT

# Get the latest production commit hash
LATEST_PRODUCTION=$(git rev-parse origin/production)
echo "latest_production=$LATEST_PRODUCTION" >> $GITHUB_OUTPUT

echo "Staging commits ahead of production: $COMMITS_AHEAD"
echo "Latest staging: $LATEST_STAGING"
echo "Latest production: $LATEST_PRODUCTION"

- name: Reset staging to match production
if: steps.check-staging.outputs.commits_ahead != '0'
run: |
echo "✅ Resetting staging to match production"
git checkout staging
git reset --hard origin/production
git push --force-with-lease origin staging
echo "✅ Staging reset complete"

- name: Log successful staging reset
if: steps.check-staging.outputs.commits_ahead != '0'
run: |
echo "🎉 Staging successfully reset to match production"
echo "This ensures clean workflow for future feature branches"

- name: Log when staging is already clean
if: steps.check-staging.outputs.commits_ahead == '0'
run: |
echo "ℹ️ Staging is already clean (matches production)"
echo "No reset needed"
62 changes: 62 additions & 0 deletions packages/components/credentials/AtlassianOauth.credential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { INodeParams, INodeCredential } from '../src/Interface'

class AtlassianOauth implements INodeCredential {
label: string
name: string
version: number
description: string
inputs: INodeParams[]
oauthStrategy: string

constructor() {
this.label = 'Atlassian OAuth'
this.name = 'atlassianOAuth'
this.version = 1.0
this.description = 'Atlassian OAuth Credentials for JIRA and Confluence'
this.oauthStrategy = 'atlassian'
this.inputs = [
{
label: 'Access Token',
name: 'access_token',
type: 'password',
optional: false,
disabled: true,
placeholder: 'Will be populated after OAuth authentication'
},
{
label: 'Refresh Token',
name: 'refresh_token',
type: 'password',
optional: false,
disabled: true,
placeholder: 'Will be populated after OAuth authentication'
},
{
label: 'Token Expiration Time',
name: 'expiration_time',
type: 'password',
optional: false,
disabled: true,
placeholder: 'Will be populated after OAuth authentication'
},
{
label: 'MCP Client ID',
name: 'mcp_client_id',
type: 'password',
optional: true,
disabled: true,
placeholder: 'MCP client ID (for MCP OAuth)'
},
{
label: 'MCP Client Secret',
name: 'mcp_client_secret',
type: 'password',
optional: true,
disabled: true,
placeholder: 'MCP client secret (for MCP OAuth)'
}
]
}
}

module.exports = { credClass: AtlassianOauth }
145 changes: 145 additions & 0 deletions packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Atlassian Remote MCP Server Node
*
* This file implements an Atlassian MCP server node that uses the remote MCP server
* at https://mcp.atlassian.com/v1/sse with OAuth 2.0 bearer token authentication.
*
* Key features:
* - Uses atlassianOAuth credential with access_token, refresh_token, expiration_time
* - 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
*
* Required environment variables:
* - ATLASSIAN_CLIENT_ID
* - ATLASSIAN_CLIENT_SECRET
*
* For more details and a template, see MCP/README.md.
* All comments and documentation must be in English.
*/
import { Tool } from '@langchain/core/tools'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { MCPToolkit } from '../core'
import { getCredentialData } from '../../../../src/utils'

class Atlassian_MCP implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
documentation: string
credential: INodeParams
inputs: INodeParams[]
tags: string[]
requiresOAuthRefresh: boolean

constructor() {
this.label = 'Atlassian MCP'
this.name = 'atlassianMcp'
this.version = 1.0
this.type = 'Atlassian MCP Tool'
this.icon = 'atlassian.svg'
this.category = 'MCP Servers'
this.tags = ['AAI']
this.description = 'MCP server that integrates with Atlassian JIRA and Confluence using OAuth authentication'
this.documentation = 'https://support.atlassian.com/rovo/docs/getting-started-with-the-atlassian-remote-mcp-server/'
this.requiresOAuthRefresh = true
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['atlassianOAuth']
}
this.inputs = [
{
label: 'Available Actions',
name: 'mcpActions',
type: 'asyncMultiOptions',
loadMethod: 'listActions',
refresh: true
}
]
this.baseClasses = ['Tool']
}

//@ts-ignore
loadMethods = {
listActions: async (nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> => {
try {
// Check if credential exists first
if (!nodeData.credential) {
return [
{
label: 'No Credential Selected',
name: 'no_credential',
description: 'Please select an Atlassian OAuth credential first'
}
]
}

const toolset = await this.getTools(nodeData, options)
toolset.sort((a, b) => a.name.localeCompare(b.name))

return toolset.map(({ name, ...rest }) => ({
label: name.toUpperCase(),
name: name,
description: rest.description || name
}))
} catch (error) {
console.error('Error loading Atlassian MCP actions:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
return [
{
label: 'Error Loading Actions',
name: 'error',
description: `Failed to load actions: ${errorMessage}. Please check your OAuth credential setup.`
}
]
}
}
}

async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<Tool[]> {
const tools = await this.getTools(nodeData, options)

const _mcpActions = nodeData.inputs?.mcpActions
let mcpActions = []
if (_mcpActions) {
try {
mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions
} catch (error) {
console.error('Error parsing mcp actions:', error)
}
}

return tools.filter((tool) => mcpActions.includes(tool.name))
}

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) {
throw new Error('Access token not found in credential data')
}

// For remote MCP servers, we need to provide the URL directly
const serverParams = {
url: 'https://mcp.atlassian.com/v1/sse'
}

const toolkit = new MCPToolkit(serverParams, 'sse', credentialData.access_token)
await toolkit.initialize()

const tools = toolkit.tools ?? []

return tools
}
}

module.exports = { nodeClass: Atlassian_MCP }
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 24 additions & 3 deletions packages/components/nodes/tools/MCP/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export class MCPToolkit extends BaseToolkit {
client: Client | null = null
serverParams: StdioServerParameters | any
transportType: 'stdio' | 'sse'
constructor(serverParams: StdioServerParameters | any, transportType: 'stdio' | 'sse') {
accessToken: string | null = null
constructor(serverParams: StdioServerParameters | any, transportType: 'stdio' | 'sse', accessToken: string | null = null) {
super()
this.serverParams = serverParams
this.transportType = transportType
this.accessToken = accessToken
}

// Method to create a new client with transport
Expand Down Expand Up @@ -52,12 +54,31 @@ export class MCPToolkit extends BaseToolkit {
}

const baseUrl = new URL(this.serverParams.url)

try {
transport = new StreamableHTTPClientTransport(baseUrl)
console.log('Attempting StreamableHTTP connection to:', baseUrl.href)
console.log('Using token length:', this.accessToken?.length)
transport = new StreamableHTTPClientTransport(baseUrl, {
requestInit: {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
}
})
await client.connect(transport)
console.log('StreamableHTTP connection successful')
} catch (error) {
transport = new SSEClientTransport(baseUrl)
console.log('StreamableHTTP failed, trying SSE. Error:', error.message)

transport = new SSEClientTransport(baseUrl, {
requestInit: {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
}
})
await client.connect(transport)
console.log('SSE connection successful')
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"lodash": "^4.17.21",
"lunary": "^0.7.15",
"mammoth": "^1.10.0",
"mcp-remote": "^0.1.18",
"meilisearch": "^0.41.0",
"moment": "^2.30.1",
"mongodb": "6.3.0",
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export interface INode extends INodeProperties {
credential?: INodeParams
inputs?: INodeParams[]
output?: INodeOutputsValue[]
requiresOAuthRefresh?: boolean
loadMethods?: {
[key: string]: (nodeData: INodeData, options?: ICommonObject) => Promise<INodeOptionsValue[]>
}
Expand Down
7 changes: 7 additions & 0 deletions packages/docs/static/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# robots.txt for https://answeragent.ai
# Allow all crawlers to access all content
User-agent: *
Allow: /

# Sitemap
Sitemap: https://answeragent.ai/sitemap.xml
2 changes: 2 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.859.0",
"@aws-sdk/s3-request-presigner": "^3.859.0",
"@braintree/sanitize-url": "^7.1.1",
"@contentful/rich-text-from-markdown": "^15.18.10",
"@google-cloud/logging-winston": "^6.0.1",
"@oclif/core": "4.0.7",
Expand Down Expand Up @@ -120,6 +121,7 @@
"nim-container-manager": "^1.0.5",
"openai": "4.96.0",
"passport": "^0.7.0",
"passport-custom": "^1.1.1",
"passport-google-oauth20": "^2.0.0",
"passport-oauth2": "^1.8.0",
"pg": "^8.16.3",
Expand Down
Loading