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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jupiterone/jupiterone-mcp",
"version": "0.0.11",
"version": "0.0.12",
"description": "Model Context Protocol server for JupiterOne account rules and rule details",
"main": "dist/index.js",
"bin": {
Expand Down
26 changes: 16 additions & 10 deletions src/__tests__/dashboard-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,32 @@ describe('Dashboard URL Construction', () => {
it('should construct URL with provided subdomain', () => {
const dashboardId = '6101fdba-e7bb-4f64-a9fb-62ba5f0641a2';
const subdomain = 'ripple-4e';

const url = dashboardService.constructDashboardUrl(dashboardId, subdomain);

expect(url).toBe('https://ripple-4e.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2');

expect(url).toBe(
'https://ripple-4e.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2'
);
});

it('should default to j1 subdomain when not provided', () => {
const dashboardId = '6101fdba-e7bb-4f64-a9fb-62ba5f0641a2';

const url = dashboardService.constructDashboardUrl(dashboardId);

expect(url).toBe('https://j1.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2');

expect(url).toBe(
'https://j1.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2'
);
});

it('should handle undefined subdomain', () => {
const dashboardId = '6101fdba-e7bb-4f64-a9fb-62ba5f0641a2';

const url = dashboardService.constructDashboardUrl(dashboardId, undefined);

expect(url).toBe('https://j1.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2');

expect(url).toBe(
'https://j1.apps.us.jupiterone.io/insights/dashboards/6101fdba-e7bb-4f64-a9fb-62ba5f0641a2'
);
});
});
});
});
25 changes: 25 additions & 0 deletions src/client/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,29 @@ export const QUERY_V2 = `
correlationId
}
}
`;

// Get entity counts by class and type
export const GET_ENTITY_COUNTS = `
query GetEntityCounts {
getEntityCounts {
classes
types
}
}
`;

// Get properties for a specific entity type
export const QUERY_PROPERTIES = `
query QueryProperties($entity: String) {
queryProperties(entity: $entity) {
id
accountId
entity
name
valueType
count
__typename
}
}
`;
8 changes: 8 additions & 0 deletions src/client/jupiterone-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,12 @@ export class JupiterOneClient {
async executeJ1qlQuery(...args: Parameters<J1qlService['executeJ1qlQuery']>) {
return this.j1qlService.executeJ1qlQuery(...args);
}

async listEntityTypes() {
return this.j1qlService.listEntityTypes();
}

async listEntityProperties(entityType: string) {
return this.j1qlService.listEntityProperties(entityType);
}
}
54 changes: 45 additions & 9 deletions src/client/services/j1ql-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLClient } from 'graphql-request';
import { CreateJ1qlFromNaturalLanguageResponse } from '../../types/jupiterone.js';
import { CREATE_J1QL_FROM_NATURAL_LANGUAGE } from '../graphql/mutations.js';
import { QUERY_V2 } from '../graphql/queries.js';
import { QUERY_V2, GET_ENTITY_COUNTS, QUERY_PROPERTIES } from '../graphql/queries.js';
import { getEnv } from '../../utils/getEnv.js';

export class J1qlService {
Expand Down Expand Up @@ -62,42 +62,78 @@ export class J1qlService {
returnComputedProperties: flags?.computedProperties,
}
);

// Handle deferred response
if (response.queryV2.type === 'deferred' && response.queryV2.url) {
// Poll the URL until results are ready
const maxAttempts = 60; // 60 attempts = 1 minute max wait
const pollInterval = 1000; // 1 second between polls

for (let attempt = 0; attempt < maxAttempts; attempt++) {
const fetchResponse = await fetch(response.queryV2.url);
if (!fetchResponse.ok) {
throw new Error(`Failed to fetch query results: ${fetchResponse.statusText}`);
}

const result = await fetchResponse.json();

// Check if the query is complete
if (result.status === 'COMPLETE' || result.status === 'FAILED') {
if (result.status === 'FAILED') {
throw new Error(result.error || 'Query execution failed');
}
return result;
}

// If still in progress, wait before next poll
if (result.status === 'IN_PROGRESS') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
continue;
}

// If we get an unexpected status, return the result as-is
return result;
}

throw new Error('Query timed out after 60 seconds');
}

return response.queryV2;
}

/**
* List all entity types in the account
*/
async listEntityTypes(): Promise<any> {
const response = await this.client.request<{
getEntityCounts: {
classes: Record<string, number>;
types: Record<string, number>;
};
}>(GET_ENTITY_COUNTS);

return {
classes: response.getEntityCounts.classes,
types: response.getEntityCounts.types,
};
}

/**
* List properties for a specific entity type
*/
async listEntityProperties(entityType: string): Promise<any[]> {
const response = await this.client.request<{
queryProperties: Array<{
id: string;
accountId: string;
entity: string;
name: string;
valueType: string;
count: number;
__typename: string;
}>;
}>(QUERY_PROPERTIES, { entity: entityType });

return response.queryProperties;
}
}
29 changes: 24 additions & 5 deletions src/descriptions/execute-j1ql-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

**Purpose**: Executes JupiterOne Query Language (J1QL) queries against your JupiterOne data and returns the results. This tool is used to directly run J1QL queries that have been created, either manually or through the natural language converter.

## Recommended Query Development Workflow

**CRITICAL**: Follow this workflow for best results when writing J1QL queries:

1. **Use `list-entity-types`** - Discover what entity classes and types are available in the account
2. **Evaluate relevant entity types** - Based on the user's request, identify which entity types you need to query
3. **Use `list-entity-properties`** - For each relevant entity type, list its available properties
4. **Run exploratory queries** - Execute simple queries with the entity types and properties to see sample values:
```
FIND <entity_type> AS e RETURN e.* LIMIT 5
```
5. **Construct final query** - Build your complete query using the discovered types, properties, and values

This systematic approach ensures your queries will return meaningful results and use the correct property names and filters.

This tool should be used when:
- You need to validate the data of a query
- You need to get results from a previously generated query
Expand Down Expand Up @@ -43,8 +58,8 @@ Unified entities typically also have additional enrichment making them valuable

```
FIND UnifiedIdentity AS identity
THAT IS << User
THAT RELATES TO AS rel (Device|Host)
THAT IS << User
THAT RELATES TO AS rel (Device|Host)
THAT IS >> UnifiedDevice AS device
RETURN identity.displayName, rel._class, device.displayName
```
Expand Down Expand Up @@ -319,6 +334,11 @@ LIMIT 10

#### Discovery Queries - ALWAYS START HERE

**Recommended**: Use the dedicated tools for faster discovery:
- **`list-entity-types`**: Get all entity classes and types with counts
- **`list-entity-properties`**: Get all properties for a specific entity type

Manual discovery queries if needed:
1. **Find all entity classes**: `FIND * AS e RETURN e._class, COUNT(e)`
2. **Explore entity properties**: `FIND EntityClass AS e RETURN e.* LIMIT 10`
3. **Discover relationships**: `FIND Entity1 THAT RELATES TO AS rel Entity2 RETURN rel._class`
Expand All @@ -340,7 +360,7 @@ Before running any J1QL query, verify:
#### Most Common Errors (Quick Reference)

1. **Missing quotes**: `name = john` → `name = 'john'`
2. **Wrong quotes**: `name = "john"` → `name = 'john'`
2. **Wrong quotes**: `name = "john"` → `name = 'john'`
3. **Alias placement**: `AS u WITH active = true` → `WITH active = true AS u`
4. **WHERE needs alias**: `WHERE active = true` → `AS u WHERE u.active = true`
5. **Undefined alias**: `FIND User RETURN u.name` → `FIND User AS u RETURN u.name`
Expand Down Expand Up @@ -392,5 +412,4 @@ Example response:
{
"data": [...query results...],
"url": "https://your-account.apps.us.jupiterone.io/home/results?search=..."
}
```
}
69 changes: 69 additions & 0 deletions src/descriptions/list-entity-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# JupiterOne Entity Properties Discoverer

**Purpose**: Lists all available properties for a specific entity type in your JupiterOne account. This tool is essential for understanding what data fields are available to query for a given entity type.

This tool should be used AFTER running `list-entity-types` to discover available entity types, and BEFORE writing J1QL queries that need to access specific properties.

## Features
- Returns all properties available for a specific entity type
- Shows property names, value types, and usage counts
- Helps you understand the data model for specific entities
- Essential for writing accurate J1QL queries with property filters

## Parameters
- `entityType`: The entity type to get properties for (e.g., `aws_instance`, `okta_user`, `github_user`)

## Example Usage

### Get properties for AWS instances:
```
entityType: "aws_instance"
```

### Get properties for Okta users:
```
entityType: "okta_user"
```

## Response Format
Returns an array of property objects, each containing:
- `id`: Unique identifier for the property
- `accountId`: The account ID
- `entity`: The entity type this property belongs to
- `name`: The property name (use this in J1QL queries)
- `valueType`: The data type of the property (e.g., "string", "number", "boolean")
- `count`: Number of entities that have this property

Example response:
```json
[
{
"id": "123456",
"accountId": "j1dev",
"entity": "okta_user",
"name": "email",
"valueType": "string",
"count": 150
},
{
"id": "123457",
"accountId": "j1dev",
"entity": "okta_user",
"name": "active",
"valueType": "boolean",
"count": 150
}
]
```

## Use Cases
- **Query Preparation**: Discover available properties before writing J1QL queries
- **Data Exploration**: Understand what data is available for specific entity types
- **Filter Development**: Find properties to use in WHERE clauses and WITH filters
- **Schema Understanding**: Learn about the data model of integrated services

## Workflow Integration
This tool is typically used as part of a three-step process:
1. Run `list-entity-types` to discover available entity types
2. Run `list-entity-properties` for relevant entity types
3. Write J1QL queries using the discovered types and properties
53 changes: 53 additions & 0 deletions src/descriptions/list-entity-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# JupiterOne Entity Types Discoverer

**Purpose**: Discovers all entity classes and types in your JupiterOne account. This tool helps you find relevant entity types when building queries or exploring your data model.

**CRITICAL**: This tool should ALWAYS be run FIRST before writing any J1QL queries. Without knowing what entity classes and types exist in your JupiterOne account, you cannot write valid queries that will return results and are more likely to hallucinate. This is the essential first step for any query-writing task.

This tool provides a comprehensive count of all entity classes and types in your account using an efficient GraphQL query.

## Features
- Returns all entity classes and types in your account
- Shows counts for each class and type
- Provides results as key-value pairs for easy lookup
- Fast and efficient - no pagination required

## Example Usage

```
list-entity-types
```

## Response Format
Returns an object with two properties:
- `classes`: Object mapping class names to their counts (e.g., `{"User": 150, "Device": 200}`)
- `types`: Object mapping type names to their counts (e.g., `{"aws_instance": 50, "github_user": 25}`)

Example response:
```json
{
"classes": {
"User": 150,
"Device": 200,
"Finding": 1234,
"DataStore": 45
},
"types": {
"aws_instance": 50,
"aws_s3_bucket": 30,
"github_user": 25,
"crowdstrike_device": 100
}
}
```

## Use Cases
- **REQUIRED FIRST STEP**: Run this before writing any J1QL queries to know what entities exist
- Discovering available entity types before writing J1QL queries
- Finding integration-specific entities (e.g., all Snyk entities)
- Understanding your data model and available entity types
- Exploring what data is available from specific integrations

## Why This Is Essential

When writing J1QL queries, you need to specify entity classes (like `User`, `Device`, `Finding`) or entity types (like `aws_instance`, `github_user`). Without running this tool first, you're essentially guessing what entities exist in your account. This tool provides the complete list of valid entity classes and types that you can query, ensuring your J1QL queries will actually find and return data.
Loading