During my internship at HubSpot, I worked on the Brand Identity team across several backend and product initiatives, including a problem that became more interesting as I moved through the stack: an AI model can write a convincing product announcement while knowing very little about the company it is writing for. Its choices about language, emphasis, and audience depend on the context available when it starts writing.
A company might prefer direct language, describe its products in a particular way, or adjust its tone for different audiences. HubSpot's public Brand Identity features capture context such as voice, tone, ideal customer profile, products, and competitive information for use in AI-generated content. That gives writing tools a shared reference for decisions that would otherwise depend on whatever the user happens to include in a prompt.
The writing process also extends beyond the application where that context lives. A person might draft an email in an AI assistant and return to HubSpot later to prepare a campaign. HubSpot's remote Model Context Protocol server provides a public foundation for connecting compatible AI clients to HubSpot data, with OAuth authentication and access governed by the user's permissions.
My work explored the Brand Identity side of that connection, from helping a model retrieve relevant context to building an interactive content-review prototype. The account below describes those engineering decisions using generalized tool names and illustrative data; the review prototype and its example interfaces describe the work I explored during the internship rather than a catalog of publicly available HubSpot features.
Getting brand context into the conversation
Model Context Protocol, or MCP, gives an AI client a standard interface for discovering tools and invoking them. In a writing workflow, a tool can retrieve brand guidance before the model starts drafting, allowing that guidance to participate in the same conversation as the user's request.
The interaction begins with an ordinary prompt, such as “Write a launch email for our new analytics feature.” The client exposes the available tool definitions to the model, the model selects a context-retrieval tool, and the result supplies information that can guide the response. The conversation remains in the AI client while the connected service supplies the requested context.
Press play, or choose a step or a node in the diagram, to follow the request.
{
"method": "tools/call",
"params": {
"name": "get_brand_context_before_writing"
}
}Synthetic payload. Keys are illustrative and are not an API schema.
The first difficulty appeared at the selection step. A tool could be present in the client's registry and still be overlooked when the model decided how to answer. Retrieving context required the model to recognize that an apparently ordinary writing task depended on information held elsewhere.
That made the tool interface part of the writing workflow itself. Its name and description needed to connect the user's intent to the capability the tool provided.
Designing an API for a model
A traditional API usually has a developer between its documentation and the decision to call it. The developer understands the application, reads the endpoint contract, and chooses where the call belongs. With a model selecting tools, that reasoning takes place against the definitions supplied in the current context.
In the behavior I observed, requests that explicitly mentioned brand voice or brand identity gave the model a clear reason to retrieve the relevant guidance. A request to write a launch email gave it a weaker signal, even though brand context was useful in both cases.
I first made the tool description more explicit about when the context should be loaded. It connected retrieval to content-generation tasks and explained why brand guidance mattered even when the user had omitted the phrase “brand voice.” Supporting instructions reinforced that relationship across the writing workflow.
The next iteration moved that intent into the tool name. A data-oriented label such as retrieve_brand_identity describes the information returned by a call. A workflow-oriented label such as get_brand_context_before_writing also gives the model a cue about when to use it. These are generalized examples of the distinction I was working toward.
“Write a launch email for our new analytics feature.”
Choose a state, or press play, to compare the three definitions.
- name
- retrieve_brand_identity
- description
- Returns the company's brand identity.
Introducing our revolutionary analytics platform. It transforms reporting for everyone.
Inspect a context field
I also simplified the externally visible tool surface. Overlapping ways to retrieve similar guidance created another choice for the model, so I reduced that overlap while keeping existing callers supported. When the clearer interface replaced the previous name in discovery, the previous entry point remained callable for compatibility.
The distinction between discoverability and compatibility was useful here. New clients could see the interface we wanted them to use, while existing integrations retained a working path. Together, the naming, description, and tool-surface changes made the relationship between a writing request and its required context more explicit.
This work changed how I approached model-facing APIs. A tool definition provides a contract for execution and a cue for selection, so its wording deserves the same attention as the implementation behind it.
Generation was only one side of the experience
Once brand context could participate in drafting, I started thinking about what a user needed when reviewing an existing document. A rewritten paragraph provides a candidate result, but the review also needs to explain how that result relates to the original.
A user may want to inspect the previous wording, understand the brand guidance behind a revision, reject a particular change, or ask for another pass on one sentence. Those decisions become harder to manage when the revised document and every explanation occupy a single stream of assistant text.
I began building an interactive Brand Voice Review prototype around that workflow. The conversation initiated the review, while an embedded interface represented the document and its changes. Selecting a revision could reveal the original wording and its explanation, with feedback actions attached to the change being inspected.
That interface required the backend to return a structured relationship between the original document, the revised document, and the explanations. A revised string alone left too much work for the display layer to reconstruct.
Separating the rewrite from the diff
One possible implementation would ask a model to perform the entire review in one call: rewrite the document, identify every edit, classify the edits, preserve formatting, explain each change, produce a summary and score, and serialize the result for the frontend.
Those tasks have different requirements. Rewriting according to brand guidance involves interpretation, while comparing two known strings requires exact accounting of their contents. Asking the model to handle both makes the document contract depend on the same probabilistic process that produces the language choices.
I separated the review into three stages. The first model call rewrote the content using brand guidance. Application code compared the original and revised strings and produced an ordered set of segments. A second model call received the changed sections and the relevant guidance to generate explanations, an overall summary, and an alignment score.
- Model call
- Deterministic code
- User-facing
- Optional enrichment
Inputs
guidance: Prefer concrete product language. Avoid exaggerated claims. original: Our revolutionary analytics platform transforms reporting.
Candidate revision
Our analytics workspace helps teams understand their data.
This division gave the model responsibility for language decisions and gave application code responsibility for the exact relationship between two documents. It also made the inputs to explanation generation more focused: the second call could reason about known changes instead of discovering them again.
The alignment score belonged to the same interpretive layer as the explanations. It summarized a model's assessment of the draft against the supplied guidance, so the interface needed to present it as a review aid rather than a measured probability of correctness.
Giving the frontend a diff it could trust
The review represented the document with four segment types. Unchanged segments carried text through directly, added segments introduced new text, removed segments retained text that had been deleted, and modified segments paired original wording with its replacement.
The contract behind those categories was more important than the labels: rendering the segments in order had to reconstruct the revised content exactly, including whitespace and line breaks. That gave the frontend a reliable document representation while retaining enough information for inspection.
The following simplified example shows the relationship. The field names illustrate the contract rather than reproducing an implementation schema.
[
{ "type": "UNCHANGED", "text": "Hello everyone.\n\n" },
{
"type": "MODIFIED",
"original": "Our revolutionary platform",
"revised": "Our analytics workspace"
},
{ "type": "UNCHANGED", "text": " helps teams.\n" }
]The unchanged values and the modified segment's revised value combine into the exact revised document. Added text contributes its new value, while removed text contributes an empty string to that reconstruction. Removed text remains available separately for the review interface.
renderRevised(segments) === revisedContentHello everyone. Our revolutionary platform helps teams.
- 0 UNCHANGED
- 1 MODIFIED
- old
- "Our revolutionary platform"
- new
- "Our analytics workspace"
- orig
- [17, 43)
- rev
- [17, 40)
- 2 UNCHANGED
Arrow keys move between segments.
Press Reconstruct to combine the revised values in order.Real documents contain headings, blank paragraphs, leading or trailing whitespace, and different line-ending conventions. A change to one sentence can sit beside any of those structures, and a segment renderer must preserve their relationship to the text around them. Consecutive edits and whitespace-only sections make that requirement especially visible.
I added tests for reconstruction across multi-paragraph content, leading and trailing whitespace, Windows line endings, whitespace-only input, and combinations of segment types. Those checks placed the exactness requirement in the backend contract, giving the frontend a representation it could render directly.
The diff established what changed, while the explanation stage interpreted why the change might fit the brand. Keeping those responsibilities separate made both easier to inspect.
Treating explanations as enrichment
The staged pipeline also exposed a distinction between the result required for a review and the information that enriches it. The rewrite provides the candidate document, and the diff provides a trustworthy representation of its changes. Explanations add useful interpretation after those two pieces exist.
If explanation generation failed after the rewrite and diff succeeded, the system could still return the revised content and structured segments. The summary, explanations, and score could remain absent from that response, preserving the completed work for the user to inspect.
Full review. Every stage succeeds. The review carries revised text, segments, explanations, and a score.
This behavior followed the dependency structure of the workflow. An explanation failure affected the interpretation layer, while a rewrite failure left the review without a revised document. The response could therefore reflect the value that had actually been produced, and the interface could make missing enrichment visible without discarding a valid diff.
Turning an MCP response into an application
Once the backend returned structured segments, the frontend could represent the review as an editable document with inspectable changes. Unchanged text remained visually quiet, modifications became selectable, and removed text could be inspected separately from the revised version.
I built the review interface in React. Selecting a modification opened a panel with the original wording and the associated explanation, while actions such as rejecting a change or requesting another revision operated on the selected part of the document. Copying the revised text needed to produce a clean document that excluded removed content and review annotations.
Introducing our revolutionary analytics platform
Teams can now build reports from the data they already collect. It transforms reporting overnight and guarantees better decisions. It is the most powerful tool on the market.
Leverage synergies across every dashboard and share reports with the people who need them.
Read the setup guide to get started.
- Context loadedpending
- Rewrite completepending
- Segments generatedpending
- Explanations attachedpending
- Response renderedpending
Run the review to inspect each change.
Appears after the review.
—
Rendering real documents revealed details that were easy to overlook in a single-paragraph example. Markdown headings needed to remain block elements, paragraph spacing had to survive segmentation, and a multiline change needed to stay associated with the correct selection target. The original and revised documents could also have different paragraph structures, so matching them by paragraph index produced fragile assumptions.
I added normalization in the React layer to handle those variations while keeping the underlying document contract intact. The interface could organize the review for display and still derive clean copied text from the actual revised values.
At that point, the tool response had become application state. The conversation provided a natural place to ask for a review, and the embedded interface provided controls suited to the decisions the user needed to make afterward.
Revising one change while preserving the others
Interactive review introduces a continuity problem as soon as a user starts approving individual changes. If a review contains eight revisions and seven have been accepted, the next request should focus on the remaining revision while carrying those earlier decisions forward.
Running the original rewrite over the entire document gives the model scope to reconsider all eight changes. From the user's perspective, that broadens a local request into another document-wide review and makes their previous decisions harder to preserve.
I added a targeted feedback flow that carried the requested change alongside the content to preserve. The backend reused the review path while constraining the next pass around the part of the document that still needed work. Accepted content became part of the next request's constraints, giving the workflow a clearer relationship to the user's accumulated decisions.
This made the interaction closer to document editing, where each pass builds on decisions already made. It also clarified the state the application needed to carry: the current revision, the selected region, the user's feedback, and the material that should remain preserved.
Tool discovery has its own semantics
Another integration issue appeared before the model invoked a writing tool. Discovery tells a client which capabilities are available, while execution attempts to use one of those capabilities. A permission condition can have a different meaning in each operation.
In the path I investigated, a capability-specific permission check could turn discovery into an authorization error. For an otherwise valid connection whose user lacked access to that capability, the useful result was a successful discovery response with an empty available-tool set.
I traced that behavior through the configuration path and adjusted the handling so discovery could express capability availability, while tool execution continued to enforce authorization. The client could then distinguish a valid connection with no relevant tools from a connection or request that had actually failed.
Precondition: valid authenticated connection; capability unavailable to this user.
After. Discovery succeeds with an empty tool list: the connection is valid and has no relevant tools. An empty list is not permission to execute an unavailable tool, and genuine authentication or request failures remain failures.
That distinction gave the client more useful information for its next decision. An empty tool list describes the capabilities of the current connection, while an error describes a failure that may require recovery. Both outcomes belong in the protocol, and each should retain its meaning.
The debugging work reinforced a pattern that ran through the project: the experience depends on the behavior surrounding the model call. Tool discovery, document contracts, partial failures, and feedback state all shape what a user can reliably do with the generated result.
Deciding what belongs in the model
The project brought together several forms of reasoning with different requirements. Brand-aware rewriting involved interpretation, tool selection depended on a clear description of intent, and explanations connected language choices back to guidance. Exact document reconstruction belonged in deterministic code, while review decisions belonged with the user.
Making those responsibilities explicit gave the workflow a structure I could test and inspect. The model produced candidate language, the diff established its relationship to the original, and the interface let the user decide which changes to keep. Optional explanations could improve the review while the underlying document remained useful on its own.
I came away with a more concrete way to think about building around language models: define the contracts that the surrounding software can enforce, give the model the context needed for its interpretive work, and preserve the user's decisions as the workflow continues. For this project, that meant carrying brand guidance into the conversation and carrying a trustworthy document through every subsequent stage of review.